diff --git a/autoload/ale/lsp.vim b/autoload/ale/lsp.vim index 0519c798..b66e93d0 100644 --- a/autoload/ale/lsp.vim +++ b/autoload/ale/lsp.vim @@ -313,6 +313,21 @@ function! ale#lsp#UpdateConfig(conn_id, buffer, config) abort return 1 endfunction +function! ale#lsp#CallInitCallbacks(conn_id) abort + let l:conn = s:connections[a:conn_id] + + " Ensure the connection is marked as initialized. + " For integration with Neovim's LSP tooling this ensures immediately + " call OnInit functions in Vim after the `on_init` callback is called. + let l:conn.initialized = 1 + + " Call capabilities callbacks queued for the project. + for l:Callback in l:conn.init_queue + call l:Callback() + endfor + + let l:conn.init_queue = [] +endfunction function! ale#lsp#HandleInitResponse(conn, response) abort if get(a:response, 'method', '') is# 'initialize' @@ -331,12 +346,7 @@ function! ale#lsp#HandleInitResponse(conn, response) abort " The initialized message must be sent before everything else. call ale#lsp#Send(a:conn.id, ale#lsp#message#Initialized()) - " Call capabilities callbacks queued for the project. - for l:Callback in a:conn.init_queue - call l:Callback() - endfor - - let a:conn.init_queue = [] + call ale#lsp#CallInitCallbacks(a:conn.id) endfunction function! ale#lsp#HandleMessage(conn_id, message) abort @@ -482,6 +492,29 @@ function! ale#lsp#StartProgram(conn_id, executable, command) abort let l:conn = s:connections[a:conn_id] let l:started = 0 + if g:ale_use_neovim_lsp_api && !l:conn.is_tsserver + " For Windows from 'cmd /s/c "foo bar"' we need 'foo bar' + let l:lsp_cmd = has('win32') + \ ? ['cmd', '/s/c/', a:command[10:-2]] + \ : a:command + + " Always call lsp.start, which will either create or re-use a + " connection. We'll set `attach` to `false` so we can later use + " our OpenDocument function to attach the buffer separately. + let l:client_id = luaeval('require("ale.lsp").start(_A)', { + \ 'name': a:conn_id, + \ 'cmd': l:lsp_cmd, + \ 'root_dir': l:conn.root, + \ 'init_options': l:conn.init_options, + \}) + + if l:client_id > 0 + let l:conn.client_id = l:client_id + endif + + return l:client_id > 0 + endif + if !has_key(l:conn, 'job_id') || !ale#job#HasOpenChannel(l:conn.job_id) let l:options = { \ 'mode': 'raw', @@ -520,6 +553,7 @@ function! ale#lsp#ConnectToAddress(conn_id, address) abort let l:conn = s:connections[a:conn_id] let l:started = 0 + " TODO: Start Neovim client here. if !has_key(l:conn, 'channel_id') || !ale#socket#IsOpen(l:conn.channel_id) let l:channel_id = ale#socket#Open(a:address, { \ 'callback': {_, mess -> ale#lsp#HandleMessage(a:conn_id, mess)}, @@ -606,6 +640,15 @@ function! ale#lsp#Send(conn_id, message) abort throw 'LSP server not initialized yet!' endif + if g:ale_use_neovim_lsp_api + return luaeval('require("ale.lsp").send_message(_A)', { + \ 'client_id': l:conn.client_id, + \ 'is_notification': a:message[0] == 1 ? v:true : v:false, + \ 'method': a:message[1], + \ 'params': get(a:message, 2, v:null) + \}) + endif + let [l:id, l:data] = ale#lsp#CreateMessageData(a:message) call s:SendMessageData(l:conn, l:data) @@ -621,11 +664,17 @@ function! ale#lsp#OpenDocument(conn_id, buffer, language_id) abort if !empty(l:conn) && !has_key(l:conn.open_documents, a:buffer) if l:conn.is_tsserver let l:message = ale#lsp#tsserver_message#Open(a:buffer) + call ale#lsp#Send(a:conn_id, l:message) + elseif g:ale_use_neovim_lsp_api + call luaeval('require("ale.lsp").buf_attach(_A)', { + \ 'bufnr': a:buffer, + \ 'client_id': l:conn.client_id, + \}) else let l:message = ale#lsp#message#DidOpen(a:buffer, a:language_id) + call ale#lsp#Send(a:conn_id, l:message) endif - call ale#lsp#Send(a:conn_id, l:message) let l:conn.open_documents[a:buffer] = getbufvar(a:buffer, 'changedtick') let l:opened = 1 endif @@ -649,11 +698,17 @@ function! ale#lsp#CloseDocument(buffer) abort if l:conn.initialized && has_key(l:conn.open_documents, a:buffer) if l:conn.is_tsserver let l:message = ale#lsp#tsserver_message#Close(a:buffer) + call ale#lsp#Send(l:conn_id, l:message) + elseif g:ale_use_neovim_lsp_api + call luaeval('require("ale.lsp").buf_detach(_A)', { + \ 'bufnr': a:buffer, + \ 'client_id': l:conn.client_id, + \}) else let l:message = ale#lsp#message#DidClose(a:buffer) + call ale#lsp#Send(l:conn_id, l:message) endif - call ale#lsp#Send(l:conn_id, l:message) call remove(l:conn.open_documents, a:buffer) let l:closed = 1 endif diff --git a/autoload/ale/lsp_linter.vim b/autoload/ale/lsp_linter.vim index 2507e400..e10cb933 100644 --- a/autoload/ale/lsp_linter.vim +++ b/autoload/ale/lsp_linter.vim @@ -488,6 +488,12 @@ function! ale#lsp_linter#StartLSP(buffer, linter, Callback) abort endfunction function! s:CheckWithLSP(linter, details) abort + if g:ale_use_neovim_lsp_api && a:linter.lsp isnot# 'tsserver' + " If running an LSP client via Neovim's API then Neovim will + " internally track buffers for changes for us, and we can stop here. + return + endif + let l:buffer = a:details.buffer let l:info = get(g:ale_buffer_info, l:buffer) @@ -546,6 +552,7 @@ function! s:OnReadyForCustomRequests(args, linter, lsp_details) abort let l:id = a:lsp_details.connection_id let l:request_id = ale#lsp#Send(l:id, a:args.message) + " TODO: Implement this whole flow with the lua API. if l:request_id > 0 && has_key(a:args, 'handler') let l:Callback = function('s:HandleLSPResponseToCustomRequests') call ale#lsp#RegisterCallback(l:id, l:Callback) diff --git a/lua/ale/lsp.lua b/lua/ale/lsp.lua new file mode 100644 index 00000000..303922f6 --- /dev/null +++ b/lua/ale/lsp.lua @@ -0,0 +1,62 @@ +local module = {} + +vim.lsp.set_log_level("debug") + +module.start = function(config) + -- Neovim's luaeval sometimes adds a Boolean key to table we need to remove. + if config.init_options[true] ~= nil then + config.init_options[true] = nil + end + + config.on_init = function(_, _) + vim.defer_fn(function() + vim.fn["ale#lsp#CallInitCallbacks"](config.name) + end, 0) + end + + return vim.lsp.start(config, { + attach = false, + silent = true, + }) +end + +module.buf_attach = function(args) + return vim.lsp.buf_attach_client(args.bufnr, args.client_id) +end + +module.buf_detach = function(args) + return vim.lsp.buf_detach_client(args.bufnr, args.client_id) +end + +-- Send a message to an LSP server. +-- Notifications do not need to be handled. +-- +-- Returns -1 when a message is sent, but no response is expected +-- 0 when the message is not sent and +-- >= 1 with the message ID when a response is expected. +module.send_message = function(args) + local client = vim.lsp.get_client_by_id(args.client_id) + + if args.is_notification then + local success = client.notify(args.method, args.params) + + if success then + return -1 + end + + return 0 + end + + -- NOTE: We aren't yet handling reponses to requests properly! + -- NOTE: There is a fourth argument for a bufnr here, and it's not + -- clear what that argument is for or why we need it. + local success, request_id = client.request(args.method, args.params) + + if success then + return request_id + end + + return 0 +end + +return module diff --git a/plugin/ale.vim b/plugin/ale.vim index 40ff84ca..d68bc0bf 100644 --- a/plugin/ale.vim +++ b/plugin/ale.vim @@ -211,6 +211,16 @@ if g:ale_use_neovim_diagnostics_api && !has('nvim-0.7') echoerr('Setting g:ale_use_neovim_diagnostics_api to 1 requires Neovim 0.7+.') endif +let g:ale_use_neovim_lsp_api = get(g:, 'ale_use_neovim_lsp_api', has('nvim-0.8')) + +" If 1, replaces ALE's use of jobs and channels to connect to language +" servers, plus the custom code, and instead hooks ALE into Neovim's built-in +" language server tools. +if g:ale_use_neovim_lsp_api && !has('nvim-0.8') + " no-custom-checks + echoerr('Setting g:ale_use_neovim_lsp_api to 1 requires Neovim 0.8+.') +endif + if g:ale_set_balloons is 1 || g:ale_set_balloons is# 'hover' call ale#balloon#Enable() endif