diff --git a/LICENSE-THIRD-PARTY b/LICENSE-THIRD-PARTY new file mode 100644 index 000000000..cf15a9c2d --- /dev/null +++ b/LICENSE-THIRD-PARTY @@ -0,0 +1,34 @@ +The vim-lsp source code bundle part of third party's code following which carry +their own copyright notices and license terms: + +* vim-lsc - https://github.com/natebosch/vim-lsc + * autoload/lsc/diff.vim + ==================================================================== + + Copyright 2017 vim-lsc authors + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/README.md b/README.md index 8ef13a7dc..fe238edb3 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,8 @@ if executable('typescript-language-server') endif ``` +vim-lsp supports incremental changes of Language Server Protocol. + ## auto-complete Refer to docs on configuring omnifunc or [asyncomplete.vim](https://github.com/prabirshrestha/asyncomplete.vim). diff --git a/autoload/lsp.vim b/autoload/lsp.vim index ac3579f55..c788aa722 100644 --- a/autoload/lsp.vim +++ b/autoload/lsp.vim @@ -4,6 +4,19 @@ let s:servers = {} " { lsp_id, server_info, init_callbacks, init_result, buffers let s:notification_callbacks = [] " { name, callback } +" This hold previous content for each language servers to make +" DidChangeTextDocumentParams. The key is buffer numbers: +" { +" 1: { +" "golsp": [ "first-line", "next-line", ... ], +" "bingo": [ "first-line", "next-line", ... ] +" }, +" 2: { +" "pyls": [ "first-line", "next-line", ... ] +" } +" } +let s:file_content = {} + " do nothing, place it here only to avoid the message augroup _lsp_silent_ autocmd! @@ -135,6 +148,7 @@ function! s:register_events() abort autocmd BufReadPost * call s:on_text_document_did_open() autocmd BufWritePost * call s:on_text_document_did_save() autocmd BufWinLeave * call s:on_text_document_did_close() + autocmd BufWipeout * call s:on_buf_wipeout(bufnr('')) autocmd InsertLeave * call s:on_text_document_did_change() autocmd CursorMoved * call s:on_cursor_moved() augroup END @@ -157,18 +171,18 @@ function! s:on_text_document_did_open() abort endfunction function! s:on_text_document_did_save() abort - call lsp#log('s:on_text_document_did_save()', bufnr('%')) let l:buf = bufnr('%') + call lsp#log('s:on_text_document_did_save()', l:buf) for l:server_name in lsp#get_whitelisted_servers() - call s:ensure_flush(bufnr('%'), l:server_name, {result->s:call_did_save(l:buf, l:server_name, result, function('s:Noop'))}) + call s:ensure_flush(l:buf, l:server_name, {result->s:call_did_save(l:buf, l:server_name, result, function('s:Noop'))}) endfor endfunction function! s:on_text_document_did_change() abort - call lsp#log('s:on_text_document_did_change()', bufnr('%')) let l:buf = bufnr('%') + call lsp#log('s:on_text_document_did_change()', l:buf) for l:server_name in lsp#get_whitelisted_servers() - call s:ensure_flush(bufnr('%'), l:server_name, function('s:Noop')) + call s:ensure_flush(l:buf, l:server_name, function('s:Noop')) endfor endfunction @@ -214,7 +228,28 @@ function! s:call_did_save(buf, server_name, result, cb) abort endfunction function! s:on_text_document_did_close() abort - call lsp#log('s:on_text_document_did_close()', bufnr('%')) + let l:buf = bufnr('%') + call lsp#log('s:on_text_document_did_close()', l:buf) +endfunction + +function! s:get_last_file_content(server_name, buf) abort + if has_key(s:file_content, a:buf) && has_key(s:file_content[a:buf], a:server_name) + return s:file_content[a:buf][a:server_name] + endif + return [] +endfunction + +function! s:update_file_content(server_name, buf, new) abort + if !has_key(s:file_content, a:buf) + let s:file_content[a:buf] = {} + endif + let s:file_content[a:buf][a:server_name] = a:new +endfunction + +function! s:on_buf_wipeout(buf) abort + if has_key(s:file_content, a:buf) + call remove(s:file_content, a:buf) + endif endfunction function! s:ensure_flush_all(buf, server_names) abort @@ -399,6 +434,30 @@ function! s:ensure_conf(buf, server_name, cb) abort call a:cb(l:msg) endfunction +function! s:text_changes(server_name, buf) abort + let l:sync_kind = lsp#capabilities#get_text_document_change_sync_kind(a:server_name) + + " When syncKind is None, return null for contentChanges. + if l:sync_kind == 0 + return v:null + endif + + " When syncKind is Incremental and previous content is saved. + if l:sync_kind == 2 && has_key(s:file_content, a:buf) + " compute diff + let l:old_content = s:get_last_file_content(a:server_name, a:buf) + let l:new_content = getbufline(a:buf, 1, '$') + let l:changes = lsp#utils#diff#compute(l:old_content, l:new_content) + call s:update_file_content(a:server_name, a:buf, l:new_content) + return [l:changes] + endif + + let l:new_content = getbufline(a:buf, 1, '$') + let l:changes = {'text': join(l:new_content, "\n")} + call s:update_file_content(a:server_name, a:buf, l:new_content) + return [l:changes] +endfunction + function! s:ensure_changed(buf, server_name, cb) abort let l:server = s:servers[a:server_name] let l:path = lsp#utils#get_buffer_uri(a:buf) @@ -418,15 +477,11 @@ function! s:ensure_changed(buf, server_name, cb) abort let l:buffer_info['changed_tick'] = l:changed_tick let l:buffer_info['version'] = l:buffer_info['version'] + 1 - " todo: support range in contentChanges - call s:send_notification(a:server_name, { \ 'method': 'textDocument/didChange', \ 'params': { \ 'textDocument': s:get_text_document_identifier(a:buf, l:buffer_info), - \ 'contentChanges': [ - \ { 'text': s:get_text_document_text(a:buf) }, - \ ], + \ 'contentChanges': s:text_changes(a:server_name, a:buf), \ } \ }) @@ -599,17 +654,8 @@ function! lsp#get_whitelisted_servers(...) abort return l:active_servers endfunction -function! s:requires_eol_at_eof(buf) abort - let l:file_ends_with_eol = getbufvar(a:buf, '&eol') - let l:vim_will_save_with_eol = !getbufvar(a:buf, '&binary') && - \ getbufvar(a:buf, '&fixeol') - return l:file_ends_with_eol || l:vim_will_save_with_eol -endfunction - function! s:get_text_document_text(buf) abort - let l:buf_fileformat = getbufvar(a:buf, '&fileformat') - let l:eol = {'unix': "\n", 'dos': "\r\n", 'mac': "\r"}[l:buf_fileformat] - return join(getbufline(a:buf, 1, '$'), l:eol).(s:requires_eol_at_eof(a:buf) ? l:eol : '') + return join(getbufline(a:buf, 1, '$'), "\n") endfunction function! s:get_text_document(buf, buffer_info) abort diff --git a/autoload/lsp/capabilities.vim b/autoload/lsp/capabilities.vim index d39715579..6ee0bc899 100644 --- a/autoload/lsp/capabilities.vim +++ b/autoload/lsp/capabilities.vim @@ -65,3 +65,23 @@ function! lsp#capabilities#get_text_document_save_registration_options(server_na endif return [0, { 'includeText': 0 }] endfunction + +" supports_did_change (boolean) +function! lsp#capabilities#get_text_document_change_sync_kind(server_name) abort + let l:capabilities = lsp#get_server_capabilities(a:server_name) + if !empty(l:capabilities) && has_key(l:capabilities, 'textDocumentSync') + if type(l:capabilities['textDocumentSync']) == type({}) + if has_key(l:capabilities['textDocumentSync'], 'change') && type(l:capabilities['textDocumentSync']) == type(1) + let l:val = l:capabilities['textDocumentSync']['change'] + return l:val >= 0 && l:val <= 2 ? l:val : 1 + else + return 1 + endif + elseif type(l:capabilities['textDocumentSync']) == type(1) + return l:capabilities['textDocumentSync'] + else + return 1 + endif + endif + return 1 +endfunction diff --git a/autoload/lsp/utils/diff.vim b/autoload/lsp/utils/diff.vim new file mode 100644 index 000000000..aef00db32 --- /dev/null +++ b/autoload/lsp/utils/diff.vim @@ -0,0 +1,115 @@ +" This is copied from https://github.com/natebosch/vim-lsc/blob/master/autoload/lsc/diff.vim +" +" Computes a simplistic diff between [old] and [new]. +" +" Returns a dict with keys `range`, `rangeLength`, and `text` matching the LSP +" definition of `TextDocumentContentChangeEvent`. +" +" Finds a single change between the common prefix, and common postfix. +function! lsp#utils#diff#compute(old, new) abort + let [l:start_line, l:start_char] = s:FirstDifference(a:old, a:new) + let [l:end_line, l:end_char] = + \ s:LastDifference(a:old[l:start_line :], a:new[l:start_line :], l:start_char) + + let l:text = s:ExtractText(a:new, l:start_line, l:start_char, l:end_line, l:end_char) + let l:length = s:Length(a:old, l:start_line, l:start_char, l:end_line, l:end_char) + + let l:adj_end_line = len(a:old) + l:end_line + let l:adj_end_char = l:end_line == 0 ? 0 : strlen(a:old[l:end_line]) + l:end_char + 1 + + let l:result = { 'range': {'start': {'line': l:start_line, 'character': l:start_char}, + \ 'end': {'line': l:adj_end_line, 'character': l:adj_end_char}}, + \ 'text': l:text, + \ 'rangeLength': l:length, + \} + + return l:result +endfunction + +" Finds the line and character of the first different character between two +" list of Strings. +function! s:FirstDifference(old, new) abort + let l:line_count = min([len(a:old), len(a:new)]) + let l:i = 0 + while l:i < l:line_count + if a:old[l:i] !=# a:new[l:i] | break | endif + let l:i += 1 + endwhile + if i >= l:line_count + return [l:line_count - 1, strlen(a:old[l:line_count - 1])] + endif + let l:old_line = a:old[l:i] + let l:new_line = a:new[l:i] + let l:length = min([strlen(l:old_line), strlen(l:new_line)]) + let l:j = 0 + while l:j < l:length + if l:old_line[l:j : l:j] !=# l:new_line[l:j : l:j] | break | endif + let l:j += 1 + endwhile + return [l:i, l:j] +endfunction + +function! s:LastDifference(old, new, start_char) abort + let l:line_count = min([len(a:old), len(a:new)]) + if l:line_count == 0 | return [0, 0] | endif + let l:i = -1 + while l:i >= -1 * l:line_count + if a:old[l:i] !=# a:new[l:i] | break | endif + let l:i -= 1 + endwhile + if l:i <= -1 * l:line_count + let l:i = -1 * l:line_count + let l:old_line = a:old[l:i][a:start_char :] + let l:new_line = a:new[l:i][a:start_char :] + else + let l:old_line = a:old[l:i] + let l:new_line = a:new[l:i] + endif + let l:length = min([strlen(l:old_line), strlen(l:new_line)]) + let l:j = -1 + while l:j >= -1 * l:length + if l:old_line[l:j : l:j] !=# l:new_line[l:j : l:j] | break | endif + let l:j -= 1 + endwhile + return [l:i, l:j] +endfunction + +function! s:ExtractText(lines, start_line, start_char, end_line, end_char) abort + if a:start_line == len(a:lines) + a:end_line + if a:end_line == 0 | return '' | endif + let l:result = a:lines[a:start_line][a:start_char : a:end_char] + " json_encode treats empty string computed this was as 'null' + if strlen(l:result) == 0 | let l:result = '' | endif + return l:result + endif + let l:result = a:lines[a:start_line][a:start_char :]."\n" + let l:adj_end_line = len(a:lines) + a:end_line + for l:line in a:lines[a:start_line + 1:a:end_line - 1] + let l:result .= l:line."\n" + endfor + if a:end_line != 0 + let l:result .= a:lines[a:end_line][:a:end_char] + endif + return l:result +endfunction + +function! s:Length(lines, start_line, start_char, end_line, end_char) + \ abort + let l:adj_end_line = len(a:lines) + a:end_line + if l:adj_end_line >= len(a:lines) + let l:adj_end_char = a:end_char - 1 + else + let l:adj_end_char = strlen(a:lines[l:adj_end_line]) + a:end_char + endif + if a:start_line == l:adj_end_line + return l:adj_end_char - a:start_char + 1 + endif + let l:result = strlen(a:lines[a:start_line]) - a:start_char + 1 + let l:line = a:start_line + 1 + while l:line < l:adj_end_line + let l:result += strlen(a:lines[l:line]) + 1 + let l:line += 1 + endwhile + let l:result += l:adj_end_char + 1 + return l:result +endfunction