From 77e3efecee4e9948044ec02caeaf6fec496c6c02 Mon Sep 17 00:00:00 2001 From: Riley Bruins Date: Sun, 31 Aug 2025 14:09:12 -0700 Subject: feat(lsp): support `textDocument/onTypeFormatting` (#34637) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements [on-type formatting](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/#textDocument_onTypeFormatting) using a `vim.on_key()` approach to listen to typed keys. It will listen to keys on the *left hand side* of mappings. The `on_key` callback is cleared when detaching the last on-type formatting client. This feature is disabled by default. Co-authored-by: Maria José Solano --- .../plugin/lsp/on_type_formatting_spec.lua | 174 +++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 test/functional/plugin/lsp/on_type_formatting_spec.lua (limited to 'test/functional/plugin') diff --git a/test/functional/plugin/lsp/on_type_formatting_spec.lua b/test/functional/plugin/lsp/on_type_formatting_spec.lua new file mode 100644 index 0000000000..211350335e --- /dev/null +++ b/test/functional/plugin/lsp/on_type_formatting_spec.lua @@ -0,0 +1,174 @@ +local t = require('test.testutil') +local n = require('test.functional.testnvim')() +local t_lsp = require('test.functional.plugin.lsp.testutil') +local retry = t.retry + +local eq = t.eq +local dedent = t.dedent +local exec_lua = n.exec_lua +local insert = n.insert +local feed = n.feed + +local clear_notrace = t_lsp.clear_notrace +local create_server_definition = t_lsp.create_server_definition + +describe('vim.lsp.on_type_formatting', function() + local text = dedent([[ + int main() { + int hi + }]]) + + before_each(function() + clear_notrace() + + exec_lua(create_server_definition) + exec_lua(function() + _G.server = _G._create_server({ + capabilities = { + documentOnTypeFormattingProvider = { + firstTriggerCharacter = '=', + }, + }, + handlers = { + ---@param params lsp.DocumentOnTypeFormattingParams + ---@param callback fun(err?: lsp.ResponseError, result?: lsp.TextEdit[]) + ['textDocument/onTypeFormatting'] = function(_, params, callback) + callback(nil, { + { + newText = ';', + range = { + start = params.position, + ['end'] = params.position, + }, + }, + }) + end, + }, + }) + + _G.server_id = vim.lsp.start({ + name = 'dummy', + cmd = _G.server.cmd, + }) + vim.lsp.on_type_formatting.enable(true, { client_id = _G.server_id }) + end) + + insert(text) + end) + + it('enables formatting on type', function() + exec_lua(function() + local win = vim.api.nvim_get_current_win() + vim.api.nvim_win_set_cursor(win, { 2, 0 }) + end) + feed('A = 5') + retry(nil, 100, function() + eq( + { + 'int main() {', + ' int hi = 5;', + '}', + }, + exec_lua(function() + return vim.api.nvim_buf_get_lines(0, 0, -1, false) + end) + ) + end) + end) + + it('works with multiple clients', function() + exec_lua(function() + vim.lsp.on_type_formatting.enable(true) + _G.server2 = _G._create_server({ + capabilities = { + documentOnTypeFormattingProvider = { + firstTriggerCharacter = '.', + moreTriggerCharacter = { '=' }, + }, + }, + handlers = { + ---@param params lsp.DocumentOnTypeFormattingParams + ---@param callback fun(err?: lsp.ResponseError, result?: lsp.TextEdit[]) + ['textDocument/onTypeFormatting'] = function(_, params, callback) + callback(nil, { + { + newText = ';', + range = { + start = params.position, + ['end'] = params.position, + }, + }, + }) + end, + }, + }) + + vim.lsp.start({ + name = 'dummy2', + cmd = _G.server2.cmd, + }) + local win = vim.api.nvim_get_current_win() + vim.api.nvim_win_set_cursor(win, { 2, 0 }) + end) + feed('A =') + retry(nil, 100, function() + eq( + { + 'int main() {', + ' int hi =;;', + '}', + }, + exec_lua(function() + return vim.api.nvim_buf_get_lines(0, 0, -1, false) + end) + ) + end) + end) + + it('can be disabled', function() + exec_lua(function() + vim.lsp.on_type_formatting.enable(false, { client_id = _G.server_id }) + local win = vim.api.nvim_get_current_win() + vim.api.nvim_win_set_cursor(win, { 2, 0 }) + end) + feed('A = 5') + eq( + { + 'int main() {', + ' int hi = 5', + '}', + }, + exec_lua(function() + return vim.api.nvim_buf_get_lines(0, 0, -1, false) + end) + ) + end) + + it('attaches to new buffers', function() + exec_lua(function() + local buf = vim.api.nvim_create_buf(true, false) + vim.api.nvim_set_current_buf(buf) + vim.api.nvim_buf_set_lines(buf, 0, -1, false, { + 'int main() {', + ' int hi', + '}', + }) + local win = vim.api.nvim_get_current_win() + vim.api.nvim_win_set_cursor(win, { 2, 0 }) + vim.lsp.buf_attach_client(buf, _G.server_id) + end) + feed('A = 5') + retry(nil, 100, function() + eq( + { + 'int main() {', + ' int hi = 5;', + '}', + }, + exec_lua(function() + return vim.api.nvim_buf_get_lines(0, 0, -1, false) + end) + ) + end) + end) +end) -- cgit