From c9eb8f9d151d4a588f498b746349d0fdcacdf76b Mon Sep 17 00:00:00 2001 From: w0rp Date: Mon, 17 Mar 2025 17:34:50 +0000 Subject: [PATCH] Start up language servers with Neovim's API Get language servers starting and displaying diagnostics with Neovim's API in Neovim 0.8 and up. With this set up, now ALE needs to take over handling diagnostics returned by the language servers. --- autoload/ale/lsp.vim | 71 ++++++++++++++++++++++++++++++++----- autoload/ale/lsp_linter.vim | 7 ++++ lua/ale/lsp.lua | 62 ++++++++++++++++++++++++++++++++ plugin/ale.vim | 10 ++++++ 4 files changed, 142 insertions(+), 8 deletions(-) create mode 100644 lua/ale/lsp.lua 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