From 005b6d638caa200711bf5960e0c0d70ba5721c94 Mon Sep 17 00:00:00 2001 From: Björn Linse Date: Fri, 7 Jun 2019 14:21:00 +0200 Subject: tree-sitter: split tree-sitter lua interface from demo code --- runtime/lua/vim/tree_sitter.lua | 47 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 runtime/lua/vim/tree_sitter.lua (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/tree_sitter.lua b/runtime/lua/vim/tree_sitter.lua new file mode 100644 index 0000000000..a4cb3f3db6 --- /dev/null +++ b/runtime/lua/vim/tree_sitter.lua @@ -0,0 +1,47 @@ +local a = vim.api + +function parse_tree(tsstate, force) + if tsstate.valid and not force then + return tsstate.tree + end + tsstate.tree = tsstate.parser:parse_buf(tsstate.bufnr) + tsstate.valid = true + return tsstate.tree +end + +local function change_cb(tsstate, ev, bufnr, tick, start_row, oldstopline, stop_row) + local start_byte = a.nvim_buf_get_offset(bufnr,start_row) + -- a bit messy, should we expose edited but not reparsed tree? + -- are multiple edits safe in general? + local root = tsstate.parser:tree():root() + -- TODO: add proper lookup function! + local inode = root:descendant_for_point_range(oldstopline+9000,0, oldstopline,0) + if inode == nil then + local stop_byte = a.nvim_buf_get_offset(bufnr,stop_row) + tsstate.parser:edit(start_byte,stop_byte,stop_byte,start_row,0,stop_row,0,stop_row,0) + else + local fakeoldstoprow, fakeoldstopcol, fakebyteoldstop = inode:start() + local fake_rows = fakeoldstoprow-oldstopline + local fakestop = stop_row+fake_rows + local fakebytestop = a.nvim_buf_get_offset(bufnr,fakestop)+fakeoldstopcol + tsstate.parser:edit(start_byte,fakebyteoldstop,fakebytestop,start_row,0,fakeoldstoprow,fakeoldstopcol,fakestop,fakeoldstopcol) + end + tsstate.valid = false +end + +function create_parser(bufnr) + if bufnr == 0 then + bufnr = a.nvim_get_current_buf() + end + local ft = a.nvim_buf_get_option(bufnr, "filetype") + local tsstate = {} + tsstate.bufnr = bufnr + tsstate.parser = vim.ts_parser(ft.."_parser.so", ft) + parse_tree(tsstate) + local function cb(ev, ...) + return change_cb(tsstate, ev, ...) + end + a.nvim_buf_attach(tsstate.bufnr, false, {on_lines=cb}) + return tsstate +end + -- cgit From 1e9e2451bef21ff705e677802d1b0980356f1f86 Mon Sep 17 00:00:00 2001 From: Björn Linse Date: Fri, 7 Jun 2019 18:19:59 +0200 Subject: tree-sitter: objectify API --- runtime/lua/vim/tree_sitter.lua | 46 ++++++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 19 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/tree_sitter.lua b/runtime/lua/vim/tree_sitter.lua index a4cb3f3db6..bbc4db5f29 100644 --- a/runtime/lua/vim/tree_sitter.lua +++ b/runtime/lua/vim/tree_sitter.lua @@ -1,47 +1,55 @@ local a = vim.api -function parse_tree(tsstate, force) - if tsstate.valid and not force then - return tsstate.tree +local Parser = {} +Parser.__index = Parser + +function Parser:parse_tree(force) + if self.valid and not force then + return self.tree end - tsstate.tree = tsstate.parser:parse_buf(tsstate.bufnr) - tsstate.valid = true - return tsstate.tree + self.tree = self._parser:parse_buf(self.bufnr) + self.valid = true + return self.tree end -local function change_cb(tsstate, ev, bufnr, tick, start_row, oldstopline, stop_row) +local function change_cb(self, ev, bufnr, tick, start_row, oldstopline, stop_row) local start_byte = a.nvim_buf_get_offset(bufnr,start_row) -- a bit messy, should we expose edited but not reparsed tree? -- are multiple edits safe in general? - local root = tsstate.parser:tree():root() + local root = self._parser:tree():root() -- TODO: add proper lookup function! local inode = root:descendant_for_point_range(oldstopline+9000,0, oldstopline,0) if inode == nil then local stop_byte = a.nvim_buf_get_offset(bufnr,stop_row) - tsstate.parser:edit(start_byte,stop_byte,stop_byte,start_row,0,stop_row,0,stop_row,0) + self._parser:edit(start_byte,stop_byte,stop_byte,start_row,0,stop_row,0,stop_row,0) else local fakeoldstoprow, fakeoldstopcol, fakebyteoldstop = inode:start() local fake_rows = fakeoldstoprow-oldstopline local fakestop = stop_row+fake_rows local fakebytestop = a.nvim_buf_get_offset(bufnr,fakestop)+fakeoldstopcol - tsstate.parser:edit(start_byte,fakebyteoldstop,fakebytestop,start_row,0,fakeoldstoprow,fakeoldstopcol,fakestop,fakeoldstopcol) + self._parser:edit(start_byte,fakebyteoldstop,fakebytestop,start_row,0,fakeoldstoprow,fakeoldstopcol,fakestop,fakeoldstopcol) end - tsstate.valid = false + self.valid = false end -function create_parser(bufnr) +local function create_parser(bufnr) if bufnr == 0 then bufnr = a.nvim_get_current_buf() end local ft = a.nvim_buf_get_option(bufnr, "filetype") - local tsstate = {} - tsstate.bufnr = bufnr - tsstate.parser = vim.ts_parser(ft.."_parser.so", ft) - parse_tree(tsstate) + local self = setmetatable({bufnr=bufnr, valid=false}, Parser) + self._parser = vim._create_ts_parser(ft.."_parser.so", ft) + self:parse_tree() local function cb(ev, ...) - return change_cb(tsstate, ev, ...) + -- TODO: use weakref to self, so that the parser is free'd is no plugin is + -- using it. + return change_cb(self, ev, ...) end - a.nvim_buf_attach(tsstate.bufnr, false, {on_lines=cb}) - return tsstate + a.nvim_buf_attach(self.bufnr, false, {on_lines=cb}) + return self end +-- TODO: weak table with reusable parser per buffer. + +return {create_parser=create_parser} + -- cgit From afba23099fccc929fd0319a9a965a7b727407c7a Mon Sep 17 00:00:00 2001 From: Björn Linse Date: Sat, 8 Jun 2019 15:51:38 +0200 Subject: tree-sitter: support pre-registration of languages --- runtime/lua/vim/tree_sitter.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/tree_sitter.lua b/runtime/lua/vim/tree_sitter.lua index bbc4db5f29..a7830bc312 100644 --- a/runtime/lua/vim/tree_sitter.lua +++ b/runtime/lua/vim/tree_sitter.lua @@ -38,7 +38,7 @@ local function create_parser(bufnr) end local ft = a.nvim_buf_get_option(bufnr, "filetype") local self = setmetatable({bufnr=bufnr, valid=false}, Parser) - self._parser = vim._create_ts_parser(ft.."_parser.so", ft) + self._parser = vim._create_ts_parser(ft) self:parse_tree() local function cb(ev, ...) -- TODO: use weakref to self, so that the parser is free'd is no plugin is -- cgit From 4ea5e63aa8c866b4fcc9d10f1a26078d2517f96a Mon Sep 17 00:00:00 2001 From: Björn Linse Date: Sun, 9 Jun 2019 13:26:48 +0200 Subject: tree-sitter: add basic testing on ci build tree-sitter c parser on ci for testing purposes --- runtime/lua/vim/tree_sitter.lua | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/tree_sitter.lua b/runtime/lua/vim/tree_sitter.lua index a7830bc312..1b5f416b67 100644 --- a/runtime/lua/vim/tree_sitter.lua +++ b/runtime/lua/vim/tree_sitter.lua @@ -32,11 +32,13 @@ local function change_cb(self, ev, bufnr, tick, start_row, oldstopline, stop_row self.valid = false end -local function create_parser(bufnr) +local function create_parser(bufnr, ft) if bufnr == 0 then bufnr = a.nvim_get_current_buf() end - local ft = a.nvim_buf_get_option(bufnr, "filetype") + if ft == nil then + ft = a.nvim_buf_get_option(bufnr, "filetype") + end local self = setmetatable({bufnr=bufnr, valid=false}, Parser) self._parser = vim._create_ts_parser(ft) self:parse_tree() @@ -51,5 +53,5 @@ end -- TODO: weak table with reusable parser per buffer. -return {create_parser=create_parser} +return {create_parser=create_parser, add_language=vim._ts_add_language} -- cgit From c8f861b739b4703b1198dc1f88b09edbeb0d9f2e Mon Sep 17 00:00:00 2001 From: Björn Linse Date: Sat, 15 Jun 2019 12:10:12 +0200 Subject: tree-sitter: rename tree_sitter => treesitter for consistency --- runtime/lua/vim/tree_sitter.lua | 57 ----------------------------------------- runtime/lua/vim/treesitter.lua | 57 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 57 deletions(-) delete mode 100644 runtime/lua/vim/tree_sitter.lua create mode 100644 runtime/lua/vim/treesitter.lua (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/tree_sitter.lua b/runtime/lua/vim/tree_sitter.lua deleted file mode 100644 index 1b5f416b67..0000000000 --- a/runtime/lua/vim/tree_sitter.lua +++ /dev/null @@ -1,57 +0,0 @@ -local a = vim.api - -local Parser = {} -Parser.__index = Parser - -function Parser:parse_tree(force) - if self.valid and not force then - return self.tree - end - self.tree = self._parser:parse_buf(self.bufnr) - self.valid = true - return self.tree -end - -local function change_cb(self, ev, bufnr, tick, start_row, oldstopline, stop_row) - local start_byte = a.nvim_buf_get_offset(bufnr,start_row) - -- a bit messy, should we expose edited but not reparsed tree? - -- are multiple edits safe in general? - local root = self._parser:tree():root() - -- TODO: add proper lookup function! - local inode = root:descendant_for_point_range(oldstopline+9000,0, oldstopline,0) - if inode == nil then - local stop_byte = a.nvim_buf_get_offset(bufnr,stop_row) - self._parser:edit(start_byte,stop_byte,stop_byte,start_row,0,stop_row,0,stop_row,0) - else - local fakeoldstoprow, fakeoldstopcol, fakebyteoldstop = inode:start() - local fake_rows = fakeoldstoprow-oldstopline - local fakestop = stop_row+fake_rows - local fakebytestop = a.nvim_buf_get_offset(bufnr,fakestop)+fakeoldstopcol - self._parser:edit(start_byte,fakebyteoldstop,fakebytestop,start_row,0,fakeoldstoprow,fakeoldstopcol,fakestop,fakeoldstopcol) - end - self.valid = false -end - -local function create_parser(bufnr, ft) - if bufnr == 0 then - bufnr = a.nvim_get_current_buf() - end - if ft == nil then - ft = a.nvim_buf_get_option(bufnr, "filetype") - end - local self = setmetatable({bufnr=bufnr, valid=false}, Parser) - self._parser = vim._create_ts_parser(ft) - self:parse_tree() - local function cb(ev, ...) - -- TODO: use weakref to self, so that the parser is free'd is no plugin is - -- using it. - return change_cb(self, ev, ...) - end - a.nvim_buf_attach(self.bufnr, false, {on_lines=cb}) - return self -end - --- TODO: weak table with reusable parser per buffer. - -return {create_parser=create_parser, add_language=vim._ts_add_language} - diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua new file mode 100644 index 0000000000..1b5f416b67 --- /dev/null +++ b/runtime/lua/vim/treesitter.lua @@ -0,0 +1,57 @@ +local a = vim.api + +local Parser = {} +Parser.__index = Parser + +function Parser:parse_tree(force) + if self.valid and not force then + return self.tree + end + self.tree = self._parser:parse_buf(self.bufnr) + self.valid = true + return self.tree +end + +local function change_cb(self, ev, bufnr, tick, start_row, oldstopline, stop_row) + local start_byte = a.nvim_buf_get_offset(bufnr,start_row) + -- a bit messy, should we expose edited but not reparsed tree? + -- are multiple edits safe in general? + local root = self._parser:tree():root() + -- TODO: add proper lookup function! + local inode = root:descendant_for_point_range(oldstopline+9000,0, oldstopline,0) + if inode == nil then + local stop_byte = a.nvim_buf_get_offset(bufnr,stop_row) + self._parser:edit(start_byte,stop_byte,stop_byte,start_row,0,stop_row,0,stop_row,0) + else + local fakeoldstoprow, fakeoldstopcol, fakebyteoldstop = inode:start() + local fake_rows = fakeoldstoprow-oldstopline + local fakestop = stop_row+fake_rows + local fakebytestop = a.nvim_buf_get_offset(bufnr,fakestop)+fakeoldstopcol + self._parser:edit(start_byte,fakebyteoldstop,fakebytestop,start_row,0,fakeoldstoprow,fakeoldstopcol,fakestop,fakeoldstopcol) + end + self.valid = false +end + +local function create_parser(bufnr, ft) + if bufnr == 0 then + bufnr = a.nvim_get_current_buf() + end + if ft == nil then + ft = a.nvim_buf_get_option(bufnr, "filetype") + end + local self = setmetatable({bufnr=bufnr, valid=false}, Parser) + self._parser = vim._create_ts_parser(ft) + self:parse_tree() + local function cb(ev, ...) + -- TODO: use weakref to self, so that the parser is free'd is no plugin is + -- using it. + return change_cb(self, ev, ...) + end + a.nvim_buf_attach(self.bufnr, false, {on_lines=cb}) + return self +end + +-- TODO: weak table with reusable parser per buffer. + +return {create_parser=create_parser, add_language=vim._ts_add_language} + -- cgit From d24dec596c25690aba0aca658546ffdfcc6a952c Mon Sep 17 00:00:00 2001 From: Björn Linse Date: Sat, 15 Jun 2019 14:05:35 +0200 Subject: tree-sitter: inspect language --- runtime/lua/vim/treesitter.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua index 1b5f416b67..3a1b1fc4b3 100644 --- a/runtime/lua/vim/treesitter.lua +++ b/runtime/lua/vim/treesitter.lua @@ -53,5 +53,9 @@ end -- TODO: weak table with reusable parser per buffer. -return {create_parser=create_parser, add_language=vim._ts_add_language} +return { + create_parser=create_parser, + add_language=vim._ts_add_language, + inspect_language=vim._ts_inspect_language, +} -- cgit From 167a1cfdef0c4b3526830ad0356f06bf480df6af Mon Sep 17 00:00:00 2001 From: Björn Linse Date: Mon, 17 Jun 2019 21:46:31 +0200 Subject: tree-sitter: improve parser API (shared parser between plugins) --- runtime/lua/vim/treesitter.lua | 57 +++++++++++++++++++++++++++++++----------- 1 file changed, 42 insertions(+), 15 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua index 3a1b1fc4b3..f26d63d6ce 100644 --- a/runtime/lua/vim/treesitter.lua +++ b/runtime/lua/vim/treesitter.lua @@ -3,8 +3,13 @@ local a = vim.api local Parser = {} Parser.__index = Parser -function Parser:parse_tree(force) - if self.valid and not force then +-- TODO(bfredl): currently we retain parsers for the lifetime of the buffer. +-- Consider use weak references to release parser if all plugins are done with +-- it. +local parsers = {} + +function Parser:parse() + if self.valid then return self.tree end self.tree = self._parser:parse_buf(self.bufnr) @@ -12,7 +17,7 @@ function Parser:parse_tree(force) return self.tree end -local function change_cb(self, ev, bufnr, tick, start_row, oldstopline, stop_row) +local function on_lines(self, bufnr, _, start_row, oldstopline, stop_row) local start_byte = a.nvim_buf_get_offset(bufnr,start_row) -- a bit messy, should we expose edited but not reparsed tree? -- are multiple edits safe in general? @@ -21,41 +26,63 @@ local function change_cb(self, ev, bufnr, tick, start_row, oldstopline, stop_row local inode = root:descendant_for_point_range(oldstopline+9000,0, oldstopline,0) if inode == nil then local stop_byte = a.nvim_buf_get_offset(bufnr,stop_row) - self._parser:edit(start_byte,stop_byte,stop_byte,start_row,0,stop_row,0,stop_row,0) + self._parser:edit(start_byte,stop_byte,stop_byte, + start_row,0,stop_row,0,stop_row,0) else local fakeoldstoprow, fakeoldstopcol, fakebyteoldstop = inode:start() local fake_rows = fakeoldstoprow-oldstopline local fakestop = stop_row+fake_rows local fakebytestop = a.nvim_buf_get_offset(bufnr,fakestop)+fakeoldstopcol - self._parser:edit(start_byte,fakebyteoldstop,fakebytestop,start_row,0,fakeoldstoprow,fakeoldstopcol,fakestop,fakeoldstopcol) + self._parser:edit(start_byte, fakebyteoldstop, fakebytestop, + start_row, 0, + fakeoldstoprow, fakeoldstopcol, + fakestop, fakeoldstopcol) end self.valid = false end -local function create_parser(bufnr, ft) +local function create_parser(bufnr, ft, id) if bufnr == 0 then bufnr = a.nvim_get_current_buf() end - if ft == nil then - ft = a.nvim_buf_get_option(bufnr, "filetype") - end local self = setmetatable({bufnr=bufnr, valid=false}, Parser) self._parser = vim._create_ts_parser(ft) - self:parse_tree() - local function cb(ev, ...) + self:parse() -- TODO: use weakref to self, so that the parser is free'd is no plugin is -- using it. - return change_cb(self, ev, ...) + local function lines_cb(ev, ...) + return on_lines(self, ...) end - a.nvim_buf_attach(self.bufnr, false, {on_lines=cb}) + local detach_cb = nil + if id ~= nil then + detach_cb = function() + if parsers[id] == self then + parsers[id] = nil + end + end + end + a.nvim_buf_attach(self.bufnr, false, {on_lines=lines_cb, on_detach=detach_cb}) return self end --- TODO: weak table with reusable parser per buffer. +local function get_parser(bufnr, ft) + if bufnr == nil or bufnr == 0 then + bufnr = a.nvim_get_current_buf() + end + if ft == nil then + ft = a.nvim_buf_get_option(bufnr, "filetype") + end + local id = tostring(bufnr)..'_'..ft + + if parsers[id] == nil then + parsers[id] = create_parser(bufnr, ft, id) + end + return parsers[id] +end return { + get_parser=get_parser, create_parser=create_parser, add_language=vim._ts_add_language, inspect_language=vim._ts_inspect_language, } - -- cgit From 06ee45b9b1c14c7ce6cb23403cdbe2852d495cad Mon Sep 17 00:00:00 2001 From: Björn Linse Date: Fri, 21 Jun 2019 14:14:51 +0200 Subject: tree-sitter: fix lint, delete "demo" plugin (replaced by functional tests) --- runtime/lua/vim/treesitter.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua index f26d63d6ce..8aa170061b 100644 --- a/runtime/lua/vim/treesitter.lua +++ b/runtime/lua/vim/treesitter.lua @@ -50,7 +50,7 @@ local function create_parser(bufnr, ft, id) self:parse() -- TODO: use weakref to self, so that the parser is free'd is no plugin is -- using it. - local function lines_cb(ev, ...) + local function lines_cb(_, ...) return on_lines(self, ...) end local detach_cb = nil -- cgit From f86a2c33a2e54193598be1d8c856363e02b91e37 Mon Sep 17 00:00:00 2001 From: Björn Linse Date: Mon, 5 Aug 2019 19:37:17 +0200 Subject: tree-sitter: simplify editing using the new old_byte_size parameter --- runtime/lua/vim/treesitter.lua | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua index 8aa170061b..69b1ac8716 100644 --- a/runtime/lua/vim/treesitter.lua +++ b/runtime/lua/vim/treesitter.lua @@ -17,27 +17,12 @@ function Parser:parse() return self.tree end -local function on_lines(self, bufnr, _, start_row, oldstopline, stop_row) +local function on_lines(self, bufnr, _, start_row, old_stop_row, stop_row, old_byte_size) local start_byte = a.nvim_buf_get_offset(bufnr,start_row) - -- a bit messy, should we expose edited but not reparsed tree? - -- are multiple edits safe in general? - local root = self._parser:tree():root() - -- TODO: add proper lookup function! - local inode = root:descendant_for_point_range(oldstopline+9000,0, oldstopline,0) - if inode == nil then - local stop_byte = a.nvim_buf_get_offset(bufnr,stop_row) - self._parser:edit(start_byte,stop_byte,stop_byte, - start_row,0,stop_row,0,stop_row,0) - else - local fakeoldstoprow, fakeoldstopcol, fakebyteoldstop = inode:start() - local fake_rows = fakeoldstoprow-oldstopline - local fakestop = stop_row+fake_rows - local fakebytestop = a.nvim_buf_get_offset(bufnr,fakestop)+fakeoldstopcol - self._parser:edit(start_byte, fakebyteoldstop, fakebytestop, - start_row, 0, - fakeoldstoprow, fakeoldstopcol, - fakestop, fakeoldstopcol) - end + local stop_byte = a.nvim_buf_get_offset(bufnr,stop_row) + local old_stop_byte = start_byte + old_byte_size + self._parser:edit(start_byte,old_stop_byte,stop_byte, + start_row,0,old_stop_row,0,stop_row,0) self.valid = false end -- cgit From c844f986d4c8f34bbe60591270178504b7045a7a Mon Sep 17 00:00:00 2001 From: Björn Linse Date: Sat, 28 Sep 2019 14:04:05 +0200 Subject: tree-sitter: use "module" pattern in lua source --- runtime/lua/vim/treesitter.lua | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua index 69b1ac8716..e0202927bb 100644 --- a/runtime/lua/vim/treesitter.lua +++ b/runtime/lua/vim/treesitter.lua @@ -1,13 +1,13 @@ local a = vim.api -local Parser = {} -Parser.__index = Parser - -- TODO(bfredl): currently we retain parsers for the lifetime of the buffer. -- Consider use weak references to release parser if all plugins are done with -- it. local parsers = {} +local Parser = {} +Parser.__index = Parser + function Parser:parse() if self.valid then return self.tree @@ -17,7 +17,7 @@ function Parser:parse() return self.tree end -local function on_lines(self, bufnr, _, start_row, old_stop_row, stop_row, old_byte_size) +function Parser:_on_lines(bufnr, _, start_row, old_stop_row, stop_row, old_byte_size) local start_byte = a.nvim_buf_get_offset(bufnr,start_row) local stop_byte = a.nvim_buf_get_offset(bufnr,stop_row) local old_stop_byte = start_byte + old_byte_size @@ -26,17 +26,22 @@ local function on_lines(self, bufnr, _, start_row, old_stop_row, stop_row, old_b self.valid = false end -local function create_parser(bufnr, ft, id) +local module = { + add_language=vim._ts_add_language, + inspect_language=vim._ts_inspect_language, +} + +function module.create_parser(bufnr, ft, id) if bufnr == 0 then bufnr = a.nvim_get_current_buf() end local self = setmetatable({bufnr=bufnr, valid=false}, Parser) self._parser = vim._create_ts_parser(ft) self:parse() - -- TODO: use weakref to self, so that the parser is free'd is no plugin is + -- TODO(bfredl): use weakref to self, so that the parser is free'd is no plugin is -- using it. local function lines_cb(_, ...) - return on_lines(self, ...) + return self:_on_lines(...) end local detach_cb = nil if id ~= nil then @@ -50,7 +55,7 @@ local function create_parser(bufnr, ft, id) return self end -local function get_parser(bufnr, ft) +function module.get_parser(bufnr, ft) if bufnr == nil or bufnr == 0 then bufnr = a.nvim_get_current_buf() end @@ -60,14 +65,9 @@ local function get_parser(bufnr, ft) local id = tostring(bufnr)..'_'..ft if parsers[id] == nil then - parsers[id] = create_parser(bufnr, ft, id) + parsers[id] = module.create_parser(bufnr, ft, id) end return parsers[id] end -return { - get_parser=get_parser, - create_parser=create_parser, - add_language=vim._ts_add_language, - inspect_language=vim._ts_inspect_language, -} +return module -- cgit From 996a057fb9b4b7d791adad19f07b2f9c53a88ab5 Mon Sep 17 00:00:00 2001 From: Hirokazu Hata Date: Mon, 21 Oct 2019 23:46:28 +0900 Subject: lua/stdlib: adjust some validation messages #11271 close #11271 --- runtime/lua/vim/shared.lua | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index cd6f8a04d8..220b6c6c7c 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -44,9 +44,9 @@ end --@param plain If `true` use `sep` literally (passed to String.find) --@returns Iterator over the split components local function gsplit(s, sep, plain) - assert(type(s) == "string") - assert(type(sep) == "string") - assert(type(plain) == "boolean" or type(plain) == "nil") + assert(type(s) == "string", string.format("Expected string, got %s", type(s))) + assert(type(sep) == "string", string.format("Expected string, got %s", type(sep))) + assert(type(plain) == "boolean" or type(plain) == "nil", string.format("Expected boolean or nil, got %s", type(plain))) local start = 1 local done = false @@ -103,9 +103,8 @@ end --@param value Value to compare --@returns true if `t` contains `value` local function tbl_contains(t, value) - if type(t) ~= 'table' then - error('t must be a table') - end + assert(type(t) == 'table', string.format("Expected table, got %s", type(t))) + for _,v in ipairs(t) do if v == value then return true @@ -174,7 +173,7 @@ end --@param s String to trim --@returns String with whitespace removed from its beginning and end local function trim(s) - assert(type(s) == 'string', 'Only strings can be trimmed') + assert(type(s) == 'string', string.format("Expected string, got %s", type(s))) return s:match('^%s*(.*%S)') or '' end @@ -184,7 +183,7 @@ end --@param s String to escape --@returns %-escaped pattern string local function pesc(s) - assert(type(s) == 'string') + assert(type(s) == 'string', string.format("Expected string, got %s", type(s))) return s:gsub('[%(%)%.%%%+%-%*%?%[%]%^%$]', '%%%1') end -- cgit From c2fc4255f92157b65a2f0d5b299027195541d6b8 Mon Sep 17 00:00:00 2001 From: Hirokazu Hata Date: Wed, 23 Oct 2019 10:50:42 +0900 Subject: runtime: Use module pattern with vim/shared.lua It's a bit cumbersome for us to add an export target every time we define a new function. It's also cumbersome to care about the order of definition when creating a new function by referring to other functions in the module. --- runtime/lua/vim/shared.lua | 75 +++++++++++++++++++++------------------------- 1 file changed, 34 insertions(+), 41 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 220b6c6c7c..7727fdbab0 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -4,34 +4,37 @@ -- test-suite. If, in the future, Nvim itself is used to run the test-suite -- instead of "vanilla Lua", these functions could move to src/nvim/lua/vim.lua +local vim = {} --- Returns a deep copy of the given object. Non-table objects are copied as --- in a typical Lua assignment, whereas table objects are copied recursively. --- --@param orig Table to copy --@returns New table of copied keys and (nested) values. -local function deepcopy(orig) - error(orig) -end -local function _id(v) - return v -end -local deepcopy_funcs = { - table = function(orig) - local copy = {} - for k, v in pairs(orig) do - copy[deepcopy(k)] = deepcopy(v) - end - return copy - end, - number = _id, - string = _id, - ['nil'] = _id, - boolean = _id, -} -deepcopy = function(orig) - return deepcopy_funcs[type(orig)](orig) -end +function vim.deepcopy(orig) end -- luacheck: no unused +vim.deepcopy = (function() + local function _id(v) + return v + end + + local deepcopy_funcs = { + table = function(orig) + local copy = {} + for k, v in pairs(orig) do + copy[vim.deepcopy(k)] = vim.deepcopy(v) + end + return copy + end, + number = _id, + string = _id, + ['nil'] = _id, + boolean = _id, + } + + return function(orig) + return deepcopy_funcs[type(orig)](orig) + end +end)() --- Splits a string at each instance of a separator. --- @@ -43,7 +46,7 @@ end --@param sep Separator string or pattern --@param plain If `true` use `sep` literally (passed to String.find) --@returns Iterator over the split components -local function gsplit(s, sep, plain) +function vim.gsplit(s, sep, plain) assert(type(s) == "string", string.format("Expected string, got %s", type(s))) assert(type(sep) == "string", string.format("Expected string, got %s", type(sep))) assert(type(plain) == "boolean" or type(plain) == "nil", string.format("Expected boolean or nil, got %s", type(plain))) @@ -92,8 +95,8 @@ end --@param sep Separator string or pattern --@param plain If `true` use `sep` literally (passed to String.find) --@returns List-like table of the split components. -local function split(s,sep,plain) - local t={} for c in gsplit(s, sep, plain) do table.insert(t,c) end +function vim.split(s,sep,plain) + local t={} for c in vim.gsplit(s, sep, plain) do table.insert(t,c) end return t end @@ -102,7 +105,7 @@ end --@param t Table to check --@param value Value to compare --@returns true if `t` contains `value` -local function tbl_contains(t, value) +function vim.tbl_contains(t, value) assert(type(t) == 'table', string.format("Expected table, got %s", type(t))) for _,v in ipairs(t) do @@ -122,7 +125,7 @@ end --- - "keep": use value from the leftmost map --- - "force": use value from the rightmost map --@param ... Two or more map-like tables. -local function tbl_extend(behavior, ...) +function vim.tbl_extend(behavior, ...) if (behavior ~= 'error' and behavior ~= 'keep' and behavior ~= 'force') then error('invalid "behavior": '..tostring(behavior)) end @@ -149,7 +152,7 @@ end --- --@param t List-like table --@returns Flattened copy of the given list-like table. -local function tbl_flatten(t) +function vim.tbl_flatten(t) -- From https://github.com/premake/premake-core/blob/master/src/base/table.lua local result = {} local function _tbl_flatten(_t) @@ -172,7 +175,7 @@ end --@see https://www.lua.org/pil/20.2.html --@param s String to trim --@returns String with whitespace removed from its beginning and end -local function trim(s) +function vim.trim(s) assert(type(s) == 'string', string.format("Expected string, got %s", type(s))) return s:match('^%s*(.*%S)') or '' end @@ -182,19 +185,9 @@ end --@see https://github.com/rxi/lume --@param s String to escape --@returns %-escaped pattern string -local function pesc(s) +function vim.pesc(s) assert(type(s) == 'string', string.format("Expected string, got %s", type(s))) return s:gsub('[%(%)%.%%%+%-%*%?%[%]%^%$]', '%%%1') end -local module = { - deepcopy = deepcopy, - gsplit = gsplit, - pesc = pesc, - split = split, - tbl_contains = tbl_contains, - tbl_extend = tbl_extend, - tbl_flatten = tbl_flatten, - trim = trim, -} -return module +return vim -- cgit From 474d0bcbf724c7eed740f60391a0ed35d651e1d3 Mon Sep 17 00:00:00 2001 From: Björn Linse Date: Sun, 27 Oct 2019 20:49:03 +0100 Subject: lua: vim.rpcrequest, vim.rpcnotify, vim.NIL --- runtime/lua/vim/inspect.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/inspect.lua b/runtime/lua/vim/inspect.lua index 7cb40ca64d..0f3b908dc1 100644 --- a/runtime/lua/vim/inspect.lua +++ b/runtime/lua/vim/inspect.lua @@ -289,7 +289,7 @@ function Inspector:putValue(v) if tv == 'string' then self:puts(smartQuote(escape(v))) elseif tv == 'number' or tv == 'boolean' or tv == 'nil' or - tv == 'cdata' or tv == 'ctype' then + tv == 'cdata' or tv == 'ctype' or (vim and v == vim.NIL) then self:puts(tostring(v)) elseif tv == 'table' then self:putTable(v) -- cgit From 678a51b1da0c0535299341e7a598c080adcf8553 Mon Sep 17 00:00:00 2001 From: Hirokazu Hata Date: Mon, 28 Oct 2019 20:52:18 +0900 Subject: Lua: vim.validate() We often want to do type checking of public function arguments. - test: Rename utility_function_spec.lua to vim_spec.lua - .luacov: Map lua module names --- runtime/lua/vim/shared.lua | 74 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 7727fdbab0..9f656774a1 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -190,4 +190,78 @@ function vim.pesc(s) return s:gsub('[%(%)%.%%%+%-%*%?%[%]%^%$]', '%%%1') end +--- Type checking validation function +--- +--- Examples: +---
+---  validate({ arg={ { 'foo' }, 'table' }})                                     --> Nop
+---  validate({ arg={ 1, 'table' } })                                            --> error("arg: expected table, got number")
+---  validate({ arg1={ { 'foo' }, 'table' }, arg2={ 1, 'string' } })             --> error("arg2: expected string, got number")
+---  validate({ arg={ 3, function(a) return (a % 2) == 0  end, 'even number' }}) --> error("arg: expected even number, got 3")
+--- 
+--- +---@param ... Table or list of table. That table is "argument_name = { validation_target, type_name (, whether nil is allowed) }" +--- or "argument_name = { validation_target, validation function, expected_description }". +--- The following can be used as type_names: +--- - table or t +--- - string or s +--- - number or n +--- - boolean or b +--- - function or f +--- - nil +--- - thread +--- - userdata +function vim.validate(opt) + local function _type_name(t) + if t == 't' or t == 'table' then return 'table' end + if t == 's' or t == 'string' then return 'string' end + if t == 'n' or t == 'number' then return 'number' end + if t == 'b' or t == 'boolean' then return 'boolean' end + if t == 'f' or t == 'function' then return 'function' end + if t == 'c' then return 'callable' end + if t == 'nil' then return 'nil' end + if t == 'thread' or t == 'thread' then return 'thread' end + if t == 'userdata' then return 'userdata' end + if vim.is_callable(t) then return end + + error(string.format("Invalid type name '%s'. See \":help validate\" for more info.", t)) + end + local function _check_type(target, expected_type) + if expected_type == 'callable' then + return vim.is_callable(target) + else + return type(target) == expected_type + end + end + + for arg, v in pairs(opt) do + assert(type(arg) == 'string',string.format('Expected string, got %s', type(arg))) + assert(type(v) == 'table', string.format('Expected table, got %s', type(v))) + + local actual_arg_type = type(v[1]) + local expected_type = _type_name(v[2]) + + if expected_type then + if v[3] == true then + assert(_check_type(v[1], expected_type) or actual_arg_type == 'nil', string.format("%s: expected %s, got %s", arg, expected_type, actual_arg_type)) + else + assert(_check_type(v[1], expected_type), string.format("%s: expected %s, got %s", arg, expected_type, actual_arg_type)) + end + else + assert(v[2](v[1]), string.format("%s: expected %s, got %s", arg, v[3], v[1])) + end + end +end + +--- Return whether an object can call be used as a function. +--- +--@param f Any type of variable +--@return Boolean +function vim.is_callable(f) + if type(f) == 'function' then return true end + local m = getmetatable(f) + if m == nil then return false end + return type(m.__call) == 'function' +end + return vim -- cgit From 7aa4042d3bddf2f39d048b9ba1dd7adf2193d4eb Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Sun, 10 Nov 2019 19:58:14 -0800 Subject: Lua: vim.validate() --- runtime/lua/vim/shared.lua | 111 +++++++++++++++++++++++++-------------------- 1 file changed, 62 insertions(+), 49 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 9f656774a1..7d2523403a 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -190,68 +190,81 @@ function vim.pesc(s) return s:gsub('[%(%)%.%%%+%-%*%?%[%]%^%$]', '%%%1') end ---- Type checking validation function +--- Validates a parameter specification (types and values). --- --- Examples: ---
----  validate({ arg={ { 'foo' }, 'table' }})                                     --> Nop
----  validate({ arg={ 1, 'table' } })                                            --> error("arg: expected table, got number")
----  validate({ arg1={ { 'foo' }, 'table' }, arg2={ 1, 'string' } })             --> error("arg2: expected string, got number")
----  validate({ arg={ 3, function(a) return (a % 2) == 0  end, 'even number' }}) --> error("arg: expected even number, got 3")
+---  function user.new(name, age, hobbies)
+---    vim.validate{
+---      name={name, 'string'},
+---      age={age, 'number'},
+---      hobbies={hobbies, 'table'},
+---    }
+---    ...
+---  end
+--
+---  vim.validate{ arg1={{'foo'}, 'table'}, arg2={'foo', 'string'}}
+---     => NOP (success)
+---  vim.validate{arg1={1, 'table'}}
+---     => error("arg1: expected table, got number")
+---  vim.validate{arg1={{'foo'}, 'table'}, arg2={1, 'string'}}
+---     => error("arg2: expected string, got number")
+---  vim.validate{arg1={3, function(a) return (a % 2) == 0 end, 'even number'}}
+---     => error("arg1: expected even number, got 3")
 --- 
--- ----@param ... Table or list of table. That table is "argument_name = { validation_target, type_name (, whether nil is allowed) }" ---- or "argument_name = { validation_target, validation function, expected_description }". ---- The following can be used as type_names: ---- - table or t ---- - string or s ---- - number or n ---- - boolean or b ---- - function or f ---- - nil ---- - thread ---- - userdata -function vim.validate(opt) - local function _type_name(t) - if t == 't' or t == 'table' then return 'table' end - if t == 's' or t == 'string' then return 'string' end - if t == 'n' or t == 'number' then return 'number' end - if t == 'b' or t == 'boolean' then return 'boolean' end - if t == 'f' or t == 'function' then return 'function' end - if t == 'c' then return 'callable' end - if t == 'nil' then return 'nil' end - if t == 'thread' or t == 'thread' then return 'thread' end - if t == 'userdata' then return 'userdata' end - if vim.is_callable(t) then return end - - error(string.format("Invalid type name '%s'. See \":help validate\" for more info.", t)) - end - local function _check_type(target, expected_type) - if expected_type == 'callable' then - return vim.is_callable(target) - else - return type(target) == expected_type +--@param opt Map of parameter names to validations. Each key is a parameter +--- name; each value is a tuple in one of these forms: +--- 1. {arg_value, type_name, optional} +--- - arg_value: argument value +--- - type_name: string type name, one of: ("table", "t", "string", +--- "s", "number", "n", "boolean", "b", "function", "f", "nil", +--- "thread", "userdata") +--- - optional: (optional) boolean, if true, `nil` is valid +--- 2. {arg_value, fn, msg} +--- - arg_value: argument value +--- - fn: any function accepting one argument, returns true if and +--- only if the argument is valid +--- - msg: (optional) error string if validation fails +function vim.validate(opt) end -- luacheck: no unused +vim.validate = (function() + local type_names = { + t='table', s='string', n='number', b='boolean', f='function', c='callable', + ['table']='table', ['string']='string', ['number']='number', + ['boolean']='boolean', ['function']='function', ['callable']='callable', + ['nil']='nil', ['thread']='thread', ['userdata']='userdata', + } + local function type_name(t) + local tname = type_names[t] + if tname == nil then + error(string.format('invalid type name: %s', tostring(t))) end + return tname + end + local function is_type(val, t) + return t == 'callable' and vim.is_callable(val) or type(val) == t end - for arg, v in pairs(opt) do - assert(type(arg) == 'string',string.format('Expected string, got %s', type(arg))) - assert(type(v) == 'table', string.format('Expected table, got %s', type(v))) + return function(opt) + assert(type(opt) == 'table', string.format('opt: expected table, got %s', type(opt))) + for param_name, spec in pairs(opt) do + assert(type(spec) == 'table', string.format('%s: expected table, got %s', param_name, type(spec))) - local actual_arg_type = type(v[1]) - local expected_type = _type_name(v[2]) + local val = spec[1] -- Argument value. + local t = spec[2] -- Type name, or callable. + local optional = (true == spec[3]) - if expected_type then - if v[3] == true then - assert(_check_type(v[1], expected_type) or actual_arg_type == 'nil', string.format("%s: expected %s, got %s", arg, expected_type, actual_arg_type)) - else - assert(_check_type(v[1], expected_type), string.format("%s: expected %s, got %s", arg, expected_type, actual_arg_type)) + if not vim.is_callable(t) then -- Check type name. + if not (optional or type(val) == 'nil') and not is_type(val, type_name(t)) then + error(string.format("%s: expected %s, got %s", param_name, type_name(t), type(val))) + end + elseif not t(val) then -- Check user-provided validation function. + error(string.format("%s: expected %s, got %s", param_name, spec[3], val)) end - else - assert(v[2](v[1]), string.format("%s: expected %s, got %s", arg, v[3], v[1])) end + return true end -end +end)() --- Return whether an object can call be used as a function. --- -- cgit From a0d992785feeefaea2810dcf08564fd8bea8cab9 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Sun, 10 Nov 2019 22:18:59 -0800 Subject: Lua: Use vim.validate() instead of assert() --- runtime/lua/vim/shared.lua | 47 +++++++++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 23 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 7d2523403a..e987e07a2a 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -47,9 +47,7 @@ end)() --@param plain If `true` use `sep` literally (passed to String.find) --@returns Iterator over the split components function vim.gsplit(s, sep, plain) - assert(type(s) == "string", string.format("Expected string, got %s", type(s))) - assert(type(sep) == "string", string.format("Expected string, got %s", type(sep))) - assert(type(plain) == "boolean" or type(plain) == "nil", string.format("Expected boolean or nil, got %s", type(plain))) + vim.validate{s={s,'s'},sep={sep,'s'},plain={plain,'b',true}} local start = 1 local done = false @@ -106,7 +104,7 @@ end --@param value Value to compare --@returns true if `t` contains `value` function vim.tbl_contains(t, value) - assert(type(t) == 'table', string.format("Expected table, got %s", type(t))) + vim.validate{t={t,'t'}} for _,v in ipairs(t) do if v == value then @@ -176,7 +174,7 @@ end --@param s String to trim --@returns String with whitespace removed from its beginning and end function vim.trim(s) - assert(type(s) == 'string', string.format("Expected string, got %s", type(s))) + vim.validate{s={s,'s'}} return s:match('^%s*(.*%S)') or '' end @@ -186,13 +184,13 @@ end --@param s String to escape --@returns %-escaped pattern string function vim.pesc(s) - assert(type(s) == 'string', string.format("Expected string, got %s", type(s))) + vim.validate{s={s,'s'}} return s:gsub('[%(%)%.%%%+%-%*%?%[%]%^%$]', '%%%1') end --- Validates a parameter specification (types and values). --- ---- Examples: +--- Usage example: ---
 ---  function user.new(name, age, hobbies)
 ---    vim.validate{
@@ -202,26 +200,29 @@ end
 ---    }
 ---    ...
 ---  end
---
----  vim.validate{ arg1={{'foo'}, 'table'}, arg2={'foo', 'string'}}
+--- 
+--- +--- Examples with explicit argument values (can be run directly): +---
+---  vim.validate{arg1={{'foo'}, 'table'}, arg2={'foo', 'string'}}
 ---     => NOP (success)
+---
 ---  vim.validate{arg1={1, 'table'}}
----     => error("arg1: expected table, got number")
----  vim.validate{arg1={{'foo'}, 'table'}, arg2={1, 'string'}}
----     => error("arg2: expected string, got number")
+---     => error('arg1: expected table, got number')
+---
 ---  vim.validate{arg1={3, function(a) return (a % 2) == 0 end, 'even number'}}
----     => error("arg1: expected even number, got 3")
+---     => error('arg1: expected even number, got 3')
 --- 
--- --@param opt Map of parameter names to validations. Each key is a parameter --- name; each value is a tuple in one of these forms: ---- 1. {arg_value, type_name, optional} +--- 1. (arg_value, type_name, optional) --- - arg_value: argument value --- - type_name: string type name, one of: ("table", "t", "string", --- "s", "number", "n", "boolean", "b", "function", "f", "nil", --- "thread", "userdata") --- - optional: (optional) boolean, if true, `nil` is valid ---- 2. {arg_value, fn, msg} +--- 2. (arg_value, fn, msg) --- - arg_value: argument value --- - fn: any function accepting one argument, returns true if and --- only if the argument is valid @@ -234,14 +235,14 @@ vim.validate = (function() ['boolean']='boolean', ['function']='function', ['callable']='callable', ['nil']='nil', ['thread']='thread', ['userdata']='userdata', } - local function type_name(t) + local function _type_name(t) local tname = type_names[t] if tname == nil then error(string.format('invalid type name: %s', tostring(t))) end return tname end - local function is_type(val, t) + local function _is_type(val, t) return t == 'callable' and vim.is_callable(val) or type(val) == t end @@ -255,21 +256,21 @@ vim.validate = (function() local optional = (true == spec[3]) if not vim.is_callable(t) then -- Check type name. - if not (optional or type(val) == 'nil') and not is_type(val, type_name(t)) then - error(string.format("%s: expected %s, got %s", param_name, type_name(t), type(val))) + if (not optional or val ~= nil) and not _is_type(val, _type_name(t)) then + error(string.format("%s: expected %s, got %s", param_name, _type_name(t), type(val))) end elseif not t(val) then -- Check user-provided validation function. - error(string.format("%s: expected %s, got %s", param_name, spec[3], val)) + error(string.format("%s: expected %s, got %s", param_name, (spec[3] or '?'), val)) end end return true end end)() ---- Return whether an object can call be used as a function. +--- Returns true if object `f` can be called as a function. --- ---@param f Any type of variable ---@return Boolean +--@param f Any object +--@return true if `f` is callable, else false function vim.is_callable(f) if type(f) == 'function' then return true end local m = getmetatable(f) -- cgit From 00dc12c5d8454a2d3c6806710f63bbb446076e96 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 13 Nov 2019 12:55:26 -0800 Subject: lua LSP client: initial implementation (#11336) Mainly configuration and RPC infrastructure can be considered "done". Specific requests and their callbacks will be improved later (and also served by plugins). There are also some TODO:s for the client itself, like incremental updates. Co-authored by at-tjdevries and at-h-michael, with many review/suggestion contributions. --- runtime/lua/vim/lsp.lua | 1055 +++++++++++++++++++++++++++++ runtime/lua/vim/lsp/builtin_callbacks.lua | 296 ++++++++ runtime/lua/vim/lsp/log.lua | 95 +++ runtime/lua/vim/lsp/protocol.lua | 936 +++++++++++++++++++++++++ runtime/lua/vim/lsp/rpc.lua | 451 ++++++++++++ runtime/lua/vim/lsp/util.lua | 557 +++++++++++++++ runtime/lua/vim/shared.lua | 127 +++- runtime/lua/vim/uri.lua | 89 +++ 8 files changed, 3605 insertions(+), 1 deletion(-) create mode 100644 runtime/lua/vim/lsp.lua create mode 100644 runtime/lua/vim/lsp/builtin_callbacks.lua create mode 100644 runtime/lua/vim/lsp/log.lua create mode 100644 runtime/lua/vim/lsp/protocol.lua create mode 100644 runtime/lua/vim/lsp/rpc.lua create mode 100644 runtime/lua/vim/lsp/util.lua create mode 100644 runtime/lua/vim/uri.lua (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua new file mode 100644 index 0000000000..9dbe03dace --- /dev/null +++ b/runtime/lua/vim/lsp.lua @@ -0,0 +1,1055 @@ +local builtin_callbacks = require 'vim.lsp.builtin_callbacks' +local log = require 'vim.lsp.log' +local lsp_rpc = require 'vim.lsp.rpc' +local protocol = require 'vim.lsp.protocol' +local util = require 'vim.lsp.util' + +local nvim_err_writeln, nvim_buf_get_lines, nvim_command, nvim_buf_get_option + = vim.api.nvim_err_writeln, vim.api.nvim_buf_get_lines, vim.api.nvim_command, vim.api.nvim_buf_get_option +local uv = vim.loop +local tbl_isempty, tbl_extend = vim.tbl_isempty, vim.tbl_extend +local validate = vim.validate + +local lsp = { + protocol = protocol; + builtin_callbacks = builtin_callbacks; + util = util; + -- Allow raw RPC access. + rpc = lsp_rpc; + -- Export these directly from rpc. + rpc_response_error = lsp_rpc.rpc_response_error; + -- You probably won't need this directly, since __tostring is set for errors + -- by the RPC. + -- format_rpc_error = lsp_rpc.format_rpc_error; +} + +-- TODO consider whether 'eol' or 'fixeol' should change the nvim_buf_get_lines that send. +-- TODO improve handling of scratch buffers with LSP attached. + +local function resolve_bufnr(bufnr) + validate { bufnr = { bufnr, 'n', true } } + if bufnr == nil or bufnr == 0 then + return vim.api.nvim_get_current_buf() + end + return bufnr +end + +local function is_dir(filename) + validate{filename={filename,'s'}} + local stat = uv.fs_stat(filename) + return stat and stat.type == 'directory' or false +end + +-- TODO Use vim.wait when that is available, but provide an alternative for now. +local wait = vim.wait or function(timeout_ms, condition, interval) + validate { + timeout_ms = { timeout_ms, 'n' }; + condition = { condition, 'f' }; + interval = { interval, 'n', true }; + } + assert(timeout_ms > 0, "timeout_ms must be > 0") + local _ = log.debug() and log.debug("wait.fallback", timeout_ms) + interval = interval or 200 + local interval_cmd = "sleep "..interval.."m" + local timeout = timeout_ms + uv.now() + -- TODO is there a better way to sync this? + while true do + uv.update_time() + if condition() then + return 0 + end + if uv.now() >= timeout then + return -1 + end + nvim_command(interval_cmd) + -- vim.loop.sleep(10) + end +end +local wait_result_reason = { [-1] = "timeout"; [-2] = "interrupted"; [-3] = "error" } + +local valid_encodings = { + ["utf-8"] = 'utf-8'; ["utf-16"] = 'utf-16'; ["utf-32"] = 'utf-32'; + ["utf8"] = 'utf-8'; ["utf16"] = 'utf-16'; ["utf32"] = 'utf-32'; + UTF8 = 'utf-8'; UTF16 = 'utf-16'; UTF32 = 'utf-32'; +} + +local client_index = 0 +local function next_client_id() + client_index = client_index + 1 + return client_index +end +-- Tracks all clients created via lsp.start_client +local active_clients = {} +local all_buffer_active_clients = {} +local uninitialized_clients = {} + +local function for_each_buffer_client(bufnr, callback) + validate { + callback = { callback, 'f' }; + } + bufnr = resolve_bufnr(bufnr) + local client_ids = all_buffer_active_clients[bufnr] + if not client_ids or tbl_isempty(client_ids) then + return + end + for client_id in pairs(client_ids) do + -- This is unlikely to happen. Could only potentially happen in a race + -- condition between literally a single statement. + -- We could skip this error, but let's error for now. + local client = active_clients[client_id] + -- or error(string.format("Client %d has already shut down.", client_id)) + if client then + callback(client, client_id) + end + end +end + +-- Error codes to be used with `on_error` from |vim.lsp.start_client|. +-- Can be used to look up the string from a the number or the number +-- from the string. +lsp.client_errors = tbl_extend("error", lsp_rpc.client_errors, vim.tbl_add_reverse_lookup { + ON_INIT_CALLBACK_ERROR = table.maxn(lsp_rpc.client_errors) + 1; +}) + +local function validate_encoding(encoding) + validate { + encoding = { encoding, 's' }; + } + return valid_encodings[encoding:lower()] + or error(string.format("Invalid offset encoding %q. Must be one of: 'utf-8', 'utf-16', 'utf-32'", encoding)) +end + +local function validate_command(input) + local cmd, cmd_args + if type(input) == 'string' then + -- Use a shell to execute the command if it is a string. + cmd = vim.api.nvim_get_option('shell') + cmd_args = {vim.api.nvim_get_option('shellcmdflag'), input} + elseif vim.tbl_islist(input) then + cmd = input[1] + cmd_args = {} + -- Don't mutate our input. + for i, v in ipairs(input) do + assert(type(v) == 'string', "input arguments must be strings") + if i > 1 then + table.insert(cmd_args, v) + end + end + else + error("cmd type must be string or list.") + end + return cmd, cmd_args +end + +local function optional_validator(fn) + return function(v) + return v == nil or fn(v) + end +end + +local function validate_client_config(config) + validate { + config = { config, 't' }; + } + validate { + root_dir = { config.root_dir, is_dir, "directory" }; + callbacks = { config.callbacks, "t", true }; + capabilities = { config.capabilities, "t", true }; + -- cmd = { config.cmd, "s", false }; + cmd_cwd = { config.cmd_cwd, optional_validator(is_dir), "directory" }; + cmd_env = { config.cmd_env, "f", true }; + name = { config.name, 's', true }; + on_error = { config.on_error, "f", true }; + on_exit = { config.on_exit, "f", true }; + on_init = { config.on_init, "f", true }; + offset_encoding = { config.offset_encoding, "s", true }; + } + local cmd, cmd_args = validate_command(config.cmd) + local offset_encoding = valid_encodings.UTF16 + if config.offset_encoding then + offset_encoding = validate_encoding(config.offset_encoding) + end + return { + cmd = cmd; cmd_args = cmd_args; + offset_encoding = offset_encoding; + } +end + +local function text_document_did_open_handler(bufnr, client) + if not client.resolved_capabilities.text_document_open_close then + return + end + if not vim.api.nvim_buf_is_loaded(bufnr) then + return + end + local params = { + textDocument = { + version = 0; + uri = vim.uri_from_bufnr(bufnr); + -- TODO make sure our filetypes are compatible with languageId names. + languageId = nvim_buf_get_option(bufnr, 'filetype'); + text = table.concat(nvim_buf_get_lines(bufnr, 0, -1, false), '\n'); + } + } + client.notify('textDocument/didOpen', params) +end + + +--- Start a client and initialize it. +-- Its arguments are passed via a configuration object. +-- +-- Mandatory parameters: +-- +-- root_dir: {string} specifying the directory where the LSP server will base +-- as its rootUri on initialization. +-- +-- cmd: {string} or {list} which is the base command to execute for the LSP. A +-- string will be run using |'shell'| and a list will be interpreted as a bare +-- command with arguments passed. This is the same as |jobstart()|. +-- +-- Optional parameters: + +-- cmd_cwd: {string} specifying the directory to launch the `cmd` process. This +-- is not related to `root_dir`. By default, |getcwd()| is used. +-- +-- cmd_env: {table} specifying the environment flags to pass to the LSP on +-- spawn. This can be specified using keys like a map or as a list with `k=v` +-- pairs or both. Non-string values are coerced to a string. +-- For example: `{ "PRODUCTION=true"; "TEST=123"; PORT = 8080; HOST = "0.0.0.0"; }`. +-- +-- capabilities: A {table} which will be used instead of +-- `vim.lsp.protocol.make_client_capabilities()` which contains neovim's +-- default capabilities and passed to the language server on initialization. +-- You'll probably want to use make_client_capabilities() and modify the +-- result. +-- NOTE: +-- To send an empty dictionary, you should use +-- `{[vim.type_idx]=vim.types.dictionary}` Otherwise, it will be encoded as +-- an array. +-- +-- callbacks: A {table} of whose keys are language server method names and the +-- values are `function(err, method, params, client_id)`. +-- This will be called for: +-- - notifications from the server, where `err` will always be `nil` +-- - requests initiated by the server. For these, you can respond by returning +-- two values: `result, err`. The err must be in the format of an RPC error, +-- which is `{ code, message, data? }`. You can use |vim.lsp.rpc_response_error()| +-- to help with this. +-- - as a callback for requests initiated by the client if the request doesn't +-- explicitly specify a callback. +-- +-- init_options: A {table} of values to pass in the initialization request +-- as `initializationOptions`. See the `initialize` in the LSP spec. +-- +-- name: A {string} used in log messages. Defaults to {client_id} +-- +-- offset_encoding: One of 'utf-8', 'utf-16', or 'utf-32' which is the +-- encoding that the LSP server expects. By default, it is 'utf-16' as +-- specified in the LSP specification. The client does not verify this +-- is correct. +-- +-- on_error(code, ...): A function for handling errors thrown by client +-- operation. {code} is a number describing the error. Other arguments may be +-- passed depending on the error kind. @see |vim.lsp.client_errors| for +-- possible errors. `vim.lsp.client_errors[code]` can be used to retrieve a +-- human understandable string. +-- +-- on_init(client, initialize_result): A function which is called after the +-- request `initialize` is completed. `initialize_result` contains +-- `capabilities` and anything else the server may send. For example, `clangd` +-- sends `result.offsetEncoding` if `capabilities.offsetEncoding` was sent to +-- it. +-- +-- on_exit(code, signal, client_id): A function which is called after the +-- client has exited. code is the exit code of the process, and signal is a +-- number describing the signal used to terminate (if any). +-- +-- on_attach(client, bufnr): A function which is called after the client is +-- attached to a buffer. +-- +-- trace: 'off' | 'messages' | 'verbose' | nil passed directly to the language +-- server in the initialize request. Invalid/empty values will default to 'off' +-- +-- @returns client_id You can use |vim.lsp.get_client_by_id()| to get the +-- actual client. +-- +-- NOTE: The client is only available *after* it has been initialized, which +-- may happen after a small delay (or never if there is an error). +-- For this reason, you may want to use `on_init` to do any actions once the +-- client has been initialized. +function lsp.start_client(config) + local cleaned_config = validate_client_config(config) + local cmd, cmd_args, offset_encoding = cleaned_config.cmd, cleaned_config.cmd_args, cleaned_config.offset_encoding + + local client_id = next_client_id() + + local callbacks = tbl_extend("keep", config.callbacks or {}, builtin_callbacks) + -- Copy metatable if it has one. + if config.callbacks and config.callbacks.__metatable then + setmetatable(callbacks, getmetatable(config.callbacks)) + end + local name = config.name or tostring(client_id) + local log_prefix = string.format("LSP[%s]", name) + + local handlers = {} + + function handlers.notification(method, params) + local _ = log.debug() and log.debug('notification', method, params) + local callback = callbacks[method] + if callback then + -- Method name is provided here for convenience. + callback(nil, method, params, client_id) + end + end + + function handlers.server_request(method, params) + local _ = log.debug() and log.debug('server_request', method, params) + local callback = callbacks[method] + if callback then + local _ = log.debug() and log.debug("server_request: found callback for", method) + return callback(nil, method, params, client_id) + end + local _ = log.debug() and log.debug("server_request: no callback found for", method) + return nil, lsp.rpc_response_error(protocol.ErrorCodes.MethodNotFound) + end + + function handlers.on_error(code, err) + local _ = log.error() and log.error(log_prefix, "on_error", { code = lsp.client_errors[code], err = err }) + nvim_err_writeln(string.format('%s: Error %s: %q', log_prefix, lsp.client_errors[code], vim.inspect(err))) + if config.on_error then + local status, usererr = pcall(config.on_error, code, err) + if not status then + local _ = log.error() and log.error(log_prefix, "user on_error failed", { err = usererr }) + nvim_err_writeln(log_prefix.." user on_error failed: "..tostring(usererr)) + end + end + end + + function handlers.on_exit(code, signal) + active_clients[client_id] = nil + uninitialized_clients[client_id] = nil + for _, client_ids in pairs(all_buffer_active_clients) do + client_ids[client_id] = nil + end + if config.on_exit then + pcall(config.on_exit, code, signal, client_id) + end + end + + -- Start the RPC client. + local rpc = lsp_rpc.start(cmd, cmd_args, handlers, { + cwd = config.cmd_cwd; + env = config.cmd_env; + }) + + local client = { + id = client_id; + name = name; + rpc = rpc; + offset_encoding = offset_encoding; + callbacks = callbacks; + config = config; + } + + -- Store the uninitialized_clients for cleanup in case we exit before + -- initialize finishes. + uninitialized_clients[client_id] = client; + + local function initialize() + local valid_traces = { + off = 'off'; messages = 'messages'; verbose = 'verbose'; + } + local initialize_params = { + -- The process Id of the parent process that started the server. Is null if + -- the process has not been started by another process. If the parent + -- process is not alive then the server should exit (see exit notification) + -- its process. + processId = uv.getpid(); + -- The rootPath of the workspace. Is null if no folder is open. + -- + -- @deprecated in favour of rootUri. + rootPath = nil; + -- The rootUri of the workspace. Is null if no folder is open. If both + -- `rootPath` and `rootUri` are set `rootUri` wins. + rootUri = vim.uri_from_fname(config.root_dir); +-- rootUri = vim.uri_from_fname(vim.fn.expand("%:p:h")); + -- User provided initialization options. + initializationOptions = config.init_options; + -- The capabilities provided by the client (editor or tool) + capabilities = config.capabilities or protocol.make_client_capabilities(); + -- The initial trace setting. If omitted trace is disabled ('off'). + -- trace = 'off' | 'messages' | 'verbose'; + trace = valid_traces[config.trace] or 'off'; + -- The workspace folders configured in the client when the server starts. + -- This property is only available if the client supports workspace folders. + -- It can be `null` if the client supports workspace folders but none are + -- configured. + -- + -- Since 3.6.0 + -- workspaceFolders?: WorkspaceFolder[] | null; + -- export interface WorkspaceFolder { + -- -- The associated URI for this workspace folder. + -- uri + -- -- The name of the workspace folder. Used to refer to this + -- -- workspace folder in the user interface. + -- name + -- } + workspaceFolders = nil; + } + local _ = log.debug() and log.debug(log_prefix, "initialize_params", initialize_params) + rpc.request('initialize', initialize_params, function(init_err, result) + assert(not init_err, tostring(init_err)) + assert(result, "server sent empty result") + rpc.notify('initialized', {}) + client.initialized = true + uninitialized_clients[client_id] = nil + client.server_capabilities = assert(result.capabilities, "initialize result doesn't contain capabilities") + -- These are the cleaned up capabilities we use for dynamically deciding + -- when to send certain events to clients. + client.resolved_capabilities = protocol.resolve_capabilities(client.server_capabilities) + if config.on_init then + local status, err = pcall(config.on_init, client, result) + if not status then + pcall(handlers.on_error, lsp.client_errors.ON_INIT_CALLBACK_ERROR, err) + end + end + local _ = log.debug() and log.debug(log_prefix, "server_capabilities", client.server_capabilities) + local _ = log.info() and log.info(log_prefix, "initialized", { resolved_capabilities = client.resolved_capabilities }) + + -- Only assign after initialized. + active_clients[client_id] = client + -- If we had been registered before we start, then send didOpen This can + -- happen if we attach to buffers before initialize finishes or if + -- someone restarts a client. + for bufnr, client_ids in pairs(all_buffer_active_clients) do + if client_ids[client_id] then + client._on_attach(bufnr) + end + end + end) + end + + local function unsupported_method(method) + local msg = "server doesn't support "..method + local _ = log.warn() and log.warn(msg) + nvim_err_writeln(msg) + return lsp.rpc_response_error(protocol.ErrorCodes.MethodNotFound, msg) + end + + --- Checks capabilities before rpc.request-ing. + function client.request(method, params, callback) + if not callback then + callback = client.callbacks[method] + or error(string.format("request callback is empty and no default was found for client %s", client.name)) + end + local _ = log.debug() and log.debug(log_prefix, "client.request", client_id, method, params, callback) + -- TODO keep these checks or just let it go anyway? + if (not client.resolved_capabilities.hover and method == 'textDocument/hover') + or (not client.resolved_capabilities.signature_help and method == 'textDocument/signatureHelp') + or (not client.resolved_capabilities.goto_definition and method == 'textDocument/definition') + or (not client.resolved_capabilities.implementation and method == 'textDocument/implementation') + then + callback(unsupported_method(method), method, nil, client_id) + return + end + return rpc.request(method, params, function(err, result) + callback(err, method, result, client_id) + end) + end + + function client.notify(...) + return rpc.notify(...) + end + + function client.cancel_request(id) + validate{id = {id, 'n'}} + return rpc.notify("$/cancelRequest", { id = id }) + end + + -- Track this so that we can escalate automatically if we've alredy tried a + -- graceful shutdown + local tried_graceful_shutdown = false + function client.stop(force) + local handle = rpc.handle + if handle:is_closing() then + return + end + if force or (not client.initialized) or tried_graceful_shutdown then + handle:kill(15) + return + end + tried_graceful_shutdown = true + -- Sending a signal after a process has exited is acceptable. + rpc.request('shutdown', nil, function(err, _) + if err == nil then + rpc.notify('exit') + else + -- If there was an error in the shutdown request, then term to be safe. + handle:kill(15) + end + end) + end + + function client.is_stopped() + return rpc.handle:is_closing() + end + + function client._on_attach(bufnr) + text_document_did_open_handler(bufnr, client) + if config.on_attach then + -- TODO(ashkan) handle errors. + pcall(config.on_attach, client, bufnr) + end + end + + initialize() + + return client_id +end + +local function once(fn) + local value + return function(...) + if not value then value = fn(...) end + return value + end +end + +local text_document_did_change_handler +do + local encoding_index = { ["utf-8"] = 1; ["utf-16"] = 2; ["utf-32"] = 3; } + text_document_did_change_handler = function(_, bufnr, changedtick, + firstline, lastline, new_lastline, old_byte_size, old_utf32_size, + old_utf16_size) + local _ = log.debug() and log.debug("on_lines", bufnr, changedtick, firstline, + lastline, new_lastline, old_byte_size, old_utf32_size, old_utf16_size, nvim_buf_get_lines(bufnr, firstline, new_lastline, true)) + if old_byte_size == 0 then + return + end + -- Don't do anything if there are no clients attached. + if tbl_isempty(all_buffer_active_clients[bufnr] or {}) then + return + end + -- Lazy initialize these because clients may not even need them. + local incremental_changes = once(function(client) + local size_index = encoding_index[client.offset_encoding] + local length = select(size_index, old_byte_size, old_utf16_size, old_utf32_size) + local lines = nvim_buf_get_lines(bufnr, firstline, new_lastline, true) + -- This is necessary because we are specifying the full line including the + -- newline in range. Therefore, we must replace the newline as well. + if #lines > 0 then + table.insert(lines, '') + end + return { + range = { + start = { line = firstline, character = 0 }; + ["end"] = { line = lastline, character = 0 }; + }; + rangeLength = length; + text = table.concat(lines, '\n'); + }; + end) + local full_changes = once(function() + return { + text = table.concat(nvim_buf_get_lines(bufnr, 0, -1, false), "\n"); + }; + end) + local uri = vim.uri_from_bufnr(bufnr) + for_each_buffer_client(bufnr, function(client, _client_id) + local text_document_did_change = client.resolved_capabilities.text_document_did_change + local changes + if text_document_did_change == protocol.TextDocumentSyncKind.None then + return + --[=[ TODO(ashkan) there seem to be problems with the byte_sizes sent by + -- neovim right now so only send the full content for now. In general, we + -- can assume that servers *will* support both versions anyway, as there + -- is no way to specify the sync capability by the client. + -- See https://github.com/palantir/python-language-server/commit/cfd6675bc10d5e8dbc50fc50f90e4a37b7178821#diff-f68667852a14e9f761f6ebf07ba02fc8 for an example of pyls handling both. + --]=] + elseif true or text_document_did_change == protocol.TextDocumentSyncKind.Full then + changes = full_changes(client) + elseif text_document_did_change == protocol.TextDocumentSyncKind.Incremental then + changes = incremental_changes(client) + end + client.notify("textDocument/didChange", { + textDocument = { + uri = uri; + version = changedtick; + }; + contentChanges = { changes; } + }) + end) + end +end + +-- Buffer lifecycle handler for textDocument/didSave +function lsp._text_document_did_save_handler(bufnr) + bufnr = resolve_bufnr(bufnr) + local uri = vim.uri_from_bufnr(bufnr) + local text = once(function() + return table.concat(nvim_buf_get_lines(bufnr, 0, -1, false), '\n') + end) + for_each_buffer_client(bufnr, function(client, _client_id) + if client.resolved_capabilities.text_document_save then + local included_text + if client.resolved_capabilities.text_document_save_include_text then + included_text = text() + end + client.notify('textDocument/didSave', { + textDocument = { + uri = uri; + text = included_text; + } + }) + end + end) +end + +-- Implements the textDocument/did* notifications required to track a buffer +-- for any language server. +-- @param bufnr [number] buffer handle or 0 for current +-- @param client_id [number] the client id +function lsp.buf_attach_client(bufnr, client_id) + validate { + bufnr = {bufnr, 'n', true}; + client_id = {client_id, 'n'}; + } + bufnr = resolve_bufnr(bufnr) + local buffer_client_ids = all_buffer_active_clients[bufnr] + -- This is our first time attaching to this buffer. + if not buffer_client_ids then + buffer_client_ids = {} + all_buffer_active_clients[bufnr] = buffer_client_ids + + local uri = vim.uri_from_bufnr(bufnr) + nvim_command(string.format("autocmd BufWritePost lua vim.lsp._text_document_did_save_handler(0)", bufnr)) + -- First time, so attach and set up stuff. + vim.api.nvim_buf_attach(bufnr, false, { + on_lines = text_document_did_change_handler; + on_detach = function() + local params = { textDocument = { uri = uri; } } + for_each_buffer_client(bufnr, function(client, _client_id) + if client.resolved_capabilities.text_document_open_close then + client.notify('textDocument/didClose', params) + end + end) + all_buffer_active_clients[bufnr] = nil + end; + -- TODO if we know all of the potential clients ahead of time, then we + -- could conditionally set this. + -- utf_sizes = size_index > 1; + utf_sizes = true; + }) + end + if buffer_client_ids[client_id] then return end + -- This is our first time attaching this client to this buffer. + buffer_client_ids[client_id] = true + + local client = active_clients[client_id] + -- Send didOpen for the client if it is initialized. If it isn't initialized + -- then it will send didOpen on initialize. + if client then + client._on_attach(bufnr) + end + return true +end + +-- Check if a buffer is attached for a particular client. +-- @param bufnr [number] buffer handle or 0 for current +-- @param client_id [number] the client id +function lsp.buf_is_attached(bufnr, client_id) + return (all_buffer_active_clients[bufnr] or {})[client_id] == true +end + +-- Look up an active client by its id, returns nil if it is not yet initialized +-- or is not a valid id. +-- @param client_id number the client id. +function lsp.get_client_by_id(client_id) + return active_clients[client_id] +end + +-- Stop a client by its id, optionally with force. +-- You can also use the `stop()` function on a client if you already have +-- access to it. +-- By default, it will just ask the server to shutdown without force. +-- If you request to stop a client which has previously been requested to shutdown, +-- it will automatically force shutdown. +-- @param client_id number the client id. +-- @param force boolean (optional) whether to use force or request shutdown +function lsp.stop_client(client_id, force) + local client + client = active_clients[client_id] + if client then + client.stop(force) + return + end + client = uninitialized_clients[client_id] + if client then + client.stop(true) + end +end + +-- Returns a list of all the active clients. +function lsp.get_active_clients() + return vim.tbl_values(active_clients) +end + +-- Stop all the clients, optionally with force. +-- You can also use the `stop()` function on a client if you already have +-- access to it. +-- By default, it will just ask the server to shutdown without force. +-- If you request to stop a client which has previously been requested to shutdown, +-- it will automatically force shutdown. +-- @param force boolean (optional) whether to use force or request shutdown +function lsp.stop_all_clients(force) + for _, client in pairs(uninitialized_clients) do + client.stop(true) + end + for _, client in pairs(active_clients) do + client.stop(force) + end +end + +function lsp._vim_exit_handler() + log.info("exit_handler", active_clients) + for _, client in pairs(uninitialized_clients) do + client.stop(true) + end + -- TODO handle v:dying differently? + if tbl_isempty(active_clients) then + return + end + for _, client in pairs(active_clients) do + client.stop() + end + local wait_result = wait(500, function() return tbl_isempty(active_clients) end, 50) + if wait_result ~= 0 then + for _, client in pairs(active_clients) do + client.stop(true) + end + end +end + +nvim_command("autocmd VimLeavePre * lua vim.lsp._vim_exit_handler()") + +--- +--- Buffer level client functions. +--- + +--- Send a request to a server and return the response +-- @param bufnr [number] Buffer handle or 0 for current. +-- @param method [string] Request method name +-- @param params [table|nil] Parameters to send to the server +-- @param callback [function|nil] Request callback (or uses the client's callbacks) +-- +-- @returns: client_request_ids, cancel_all_requests +function lsp.buf_request(bufnr, method, params, callback) + validate { + bufnr = { bufnr, 'n', true }; + method = { method, 's' }; + callback = { callback, 'f', true }; + } + local client_request_ids = {} + for_each_buffer_client(bufnr, function(client, client_id) + local request_success, request_id = client.request(method, params, callback) + + -- This could only fail if the client shut down in the time since we looked + -- it up and we did the request, which should be rare. + if request_success then + client_request_ids[client_id] = request_id + end + end) + + local function cancel_all_requests() + for client_id, request_id in pairs(client_request_ids) do + local client = active_clients[client_id] + client.cancel_request(request_id) + end + end + + return client_request_ids, cancel_all_requests +end + +--- Send a request to a server and wait for the response. +-- @param bufnr [number] Buffer handle or 0 for current. +-- @param method [string] Request method name +-- @param params [string] Parameters to send to the server +-- @param timeout_ms [number|100] Maximum ms to wait for a result +-- +-- @returns: The table of {[client_id] = request_result} +function lsp.buf_request_sync(bufnr, method, params, timeout_ms) + local request_results = {} + local result_count = 0 + local function callback(err, _method, result, client_id) + request_results[client_id] = { error = err, result = result } + result_count = result_count + 1 + end + local client_request_ids, cancel = lsp.buf_request(bufnr, method, params, callback) + local expected_result_count = 0 + for _ in pairs(client_request_ids) do + expected_result_count = expected_result_count + 1 + end + local wait_result = wait(timeout_ms or 100, function() + return result_count >= expected_result_count + end, 10) + if wait_result ~= 0 then + cancel() + return nil, wait_result_reason[wait_result] + end + return request_results +end + +--- Send a notification to a server +-- @param bufnr [number] (optional): The number of the buffer +-- @param method [string]: Name of the request method +-- @param params [string]: Arguments to send to the server +-- +-- @returns nil +function lsp.buf_notify(bufnr, method, params) + validate { + bufnr = { bufnr, 'n', true }; + method = { method, 's' }; + } + for_each_buffer_client(bufnr, function(client, _client_id) + client.rpc.notify(method, params) + end) +end + +--- Function which can be called to generate omnifunc compatible completion. +function lsp.omnifunc(findstart, base) + local _ = log.debug() and log.debug("omnifunc.findstart", { findstart = findstart, base = base }) + + local bufnr = resolve_bufnr() + local has_buffer_clients = not tbl_isempty(all_buffer_active_clients[bufnr] or {}) + if not has_buffer_clients then + if findstart == 1 then + return -1 + else + return {} + end + end + + if findstart == 1 then + return vim.fn.col('.') + else + local pos = vim.api.nvim_win_get_cursor(0) + local line = assert(nvim_buf_get_lines(bufnr, pos[1]-1, pos[1], false)[1]) + local _ = log.trace() and log.trace("omnifunc.line", pos, line) + local line_to_cursor = line:sub(1, pos[2]+1) + local _ = log.trace() and log.trace("omnifunc.line_to_cursor", line_to_cursor) + local params = { + textDocument = { + uri = vim.uri_from_bufnr(bufnr); + }; + position = { + -- 0-indexed for both line and character + line = pos[1] - 1, + character = pos[2], + }; + -- The completion context. This is only available if the client specifies + -- to send this using `ClientCapabilities.textDocument.completion.contextSupport === true` + -- context = nil or { + -- triggerKind = protocol.CompletionTriggerKind.Invoked; + -- triggerCharacter = nil or ""; + -- }; + } + -- TODO handle timeout error differently? Like via an error? + local client_responses = lsp.buf_request_sync(bufnr, 'textDocument/completion', params) or {} + local matches = {} + for _, response in pairs(client_responses) do + -- TODO how to handle errors? + if not response.error then + local data = response.result + local completion_items = util.text_document_completion_list_to_complete_items(data or {}, line_to_cursor) + local _ = log.trace() and log.trace("omnifunc.completion_items", completion_items) + vim.list_extend(matches, completion_items) + end + end + return matches + end +end + +--- +--- FileType based configuration utility +--- + +local all_filetype_configs = {} + +-- Lookup a filetype config client by its name. +function lsp.get_filetype_client_by_name(name) + local config = all_filetype_configs[name] + if config.client_id then + return active_clients[config.client_id] + end +end + +local function start_filetype_config(config) + config.client_id = lsp.start_client(config) + nvim_command(string.format( + "autocmd FileType %s silent lua vim.lsp.buf_attach_client(0, %d)", + table.concat(config.filetypes, ','), + config.client_id)) + return config.client_id +end + +-- Easy configuration option for common LSP use-cases. +-- This will lazy initialize the client when the filetypes specified are +-- encountered and attach to those buffers. +-- +-- The configuration options are the same as |vim.lsp.start_client()|, but +-- with a few additions and distinctions: +-- +-- Additional parameters: +-- - filetype: {string} or {list} of filetypes to attach to. +-- - name: A unique string among all other servers configured with +-- |vim.lsp.add_filetype_config|. +-- +-- Differences: +-- - root_dir: will default to |getcwd()| +-- +function lsp.add_filetype_config(config) + -- Additional defaults. + -- Keep a copy of the user's input for debugging reasons. + local user_config = config + config = tbl_extend("force", {}, user_config) + config.root_dir = config.root_dir or uv.cwd() + -- Validate config. + validate_client_config(config) + validate { + name = { config.name, 's' }; + } + assert(config.filetype, "config must have 'filetype' key") + + local filetypes + if type(config.filetype) == 'string' then + filetypes = { config.filetype } + elseif type(config.filetype) == 'table' then + filetypes = config.filetype + assert(not tbl_isempty(filetypes), "config.filetype must not be an empty table") + else + error("config.filetype must be a string or a list of strings") + end + + if all_filetype_configs[config.name] then + -- If the client exists, then it is likely that they are doing some kind of + -- reload flow, so let's not throw an error here. + if all_filetype_configs[config.name].client_id then + -- TODO log here? It might be unnecessarily annoying. + return + end + error(string.format('A configuration with the name %q already exists. They must be unique', config.name)) + end + + all_filetype_configs[config.name] = tbl_extend("keep", config, { + client_id = nil; + filetypes = filetypes; + user_config = user_config; + }) + + nvim_command(string.format( + "autocmd FileType %s ++once silent lua vim.lsp._start_filetype_config_client(%q)", + table.concat(filetypes, ','), + config.name)) +end + +-- Create a copy of an existing configuration, and override config with values +-- from new_config. +-- This is useful if you wish you create multiple LSPs with different root_dirs +-- or other use cases. +-- +-- You can specify a new unique name, but if you do not, a unique name will be +-- created like `name-dup_count`. +-- +-- existing_name: the name of the existing config to copy. +-- new_config: the new configuration options. @see |vim.lsp.start_client()|. +-- @returns string the new name. +function lsp.copy_filetype_config(existing_name, new_config) + local config = all_filetype_configs[existing_name] + or error(string.format("Configuration with name %q doesn't exist", existing_name)) + config = tbl_extend("force", config, new_config or {}) + config.client_id = nil + config.original_config_name = existing_name + + -- If the user didn't rename it, we will. + if config.name == existing_name then + -- Create a new, unique name. + local duplicate_count = 0 + for _, conf in pairs(all_filetype_configs) do + if conf.original_config_name == existing_name then + duplicate_count = duplicate_count + 1 + end + end + config.name = string.format("%s-%d", existing_name, duplicate_count + 1) + end + print("New config name:", config.name) + lsp.add_filetype_config(config) + return config.name +end + +-- Autocmd handler to actually start the client when an applicable filetype is +-- encountered. +function lsp._start_filetype_config_client(name) + local config = all_filetype_configs[name] + -- If it exists and is running, don't make it again. + if config.client_id and active_clients[config.client_id] then + -- TODO log here? + return + end + lsp.buf_attach_client(0, start_filetype_config(config)) + return config.client_id +end + +--- +--- Miscellaneous utilities. +--- + +-- Retrieve a map from client_id to client of all active buffer clients. +-- @param bufnr [number] (optional): buffer handle or 0 for current +function lsp.buf_get_clients(bufnr) + bufnr = resolve_bufnr(bufnr) + local result = {} + for_each_buffer_client(bufnr, function(client, client_id) + result[client_id] = client + end) + return result +end + +-- Print some debug information about the current buffer clients. +-- The output of this function should not be relied upon and may change. +function lsp.buf_print_debug_info(bufnr) + print(vim.inspect(lsp.buf_get_clients(bufnr))) +end + +-- Print some debug information about all LSP related things. +-- The output of this function should not be relied upon and may change. +function lsp.print_debug_info() + print(vim.inspect({ clients = active_clients, filetype_configs = all_filetype_configs })) +end + +-- Log level dictionary with reverse lookup as well. +-- +-- Can be used to lookup the number from the name or the +-- name from the number. +-- Levels by name: 'trace', 'debug', 'info', 'warn', 'error' +-- Level numbers begin with 'trace' at 0 +lsp.log_levels = log.levels + +-- Set the log level for lsp logging. +-- Levels by name: 'trace', 'debug', 'info', 'warn', 'error' +-- Level numbers begin with 'trace' at 0 +-- @param level [number|string] the case insensitive level name or number @see |vim.lsp.log_levels| +function lsp.set_log_level(level) + if type(level) == 'string' or type(level) == 'number' then + log.set_level(level) + else + error(string.format("Invalid log level: %q", level)) + end +end + +-- Return the path of the logfile used by the LSP client. +function lsp.get_log_path() + return log.get_filename() +end + +return lsp +-- vim:sw=2 ts=2 et diff --git a/runtime/lua/vim/lsp/builtin_callbacks.lua b/runtime/lua/vim/lsp/builtin_callbacks.lua new file mode 100644 index 0000000000..cc739ce3ad --- /dev/null +++ b/runtime/lua/vim/lsp/builtin_callbacks.lua @@ -0,0 +1,296 @@ +--- Implements the following default callbacks: +-- +-- vim.api.nvim_buf_set_lines(0, 0, 0, false, vim.tbl_keys(vim.lsp.builtin_callbacks)) +-- + +-- textDocument/completion +-- textDocument/declaration +-- textDocument/definition +-- textDocument/hover +-- textDocument/implementation +-- textDocument/publishDiagnostics +-- textDocument/rename +-- textDocument/signatureHelp +-- textDocument/typeDefinition +-- TODO codeLens/resolve +-- TODO completionItem/resolve +-- TODO documentLink/resolve +-- TODO textDocument/codeAction +-- TODO textDocument/codeLens +-- TODO textDocument/documentHighlight +-- TODO textDocument/documentLink +-- TODO textDocument/documentSymbol +-- TODO textDocument/formatting +-- TODO textDocument/onTypeFormatting +-- TODO textDocument/rangeFormatting +-- TODO textDocument/references +-- window/logMessage +-- window/showMessage + +local log = require 'vim.lsp.log' +local protocol = require 'vim.lsp.protocol' +local util = require 'vim.lsp.util' +local api = vim.api + +local function split_lines(value) + return vim.split(value, '\n', true) +end + +local builtin_callbacks = {} + +-- textDocument/completion +-- https://microsoft.github.io/language-server-protocol/specification#textDocument_completion +builtin_callbacks['textDocument/completion'] = function(_, _, result) + if not result or vim.tbl_isempty(result) then + return + end + local pos = api.nvim_win_get_cursor(0) + local row, col = pos[1], pos[2] + local line = assert(api.nvim_buf_get_lines(0, row-1, row, false)[1]) + local line_to_cursor = line:sub(col+1) + + local matches = util.text_document_completion_list_to_complete_items(result, line_to_cursor) + local match_result = vim.fn.matchstrpos(line_to_cursor, '\\k\\+$') + local match_start, match_finish = match_result[2], match_result[3] + + vim.fn.complete(col + 1 - (match_finish - match_start), matches) +end + +-- textDocument/rename +builtin_callbacks['textDocument/rename'] = function(_, _, result) + if not result then return end + util.workspace_apply_workspace_edit(result) +end + +local function uri_to_bufnr(uri) + return vim.fn.bufadd((vim.uri_to_fname(uri))) +end + +builtin_callbacks['textDocument/publishDiagnostics'] = function(_, _, result) + if not result then return end + local uri = result.uri + local bufnr = uri_to_bufnr(uri) + if not bufnr then + api.nvim_err_writeln(string.format("LSP.publishDiagnostics: Couldn't find buffer for %s", uri)) + return + end + util.buf_clear_diagnostics(bufnr) + util.buf_diagnostics_save_positions(bufnr, result.diagnostics) + util.buf_diagnostics_underline(bufnr, result.diagnostics) + util.buf_diagnostics_virtual_text(bufnr, result.diagnostics) + -- util.buf_loclist(bufnr, result.diagnostics) +end + +-- textDocument/hover +-- https://microsoft.github.io/language-server-protocol/specification#textDocument_hover +-- @params MarkedString | MarkedString[] | MarkupContent +builtin_callbacks['textDocument/hover'] = function(_, _, result) + if result == nil or vim.tbl_isempty(result) then + return + end + + if result.contents ~= nil then + local markdown_lines = util.convert_input_to_markdown_lines(result.contents) + if vim.tbl_isempty(markdown_lines) then + markdown_lines = { 'No information available' } + end + util.open_floating_preview(markdown_lines, 'markdown') + end +end + +builtin_callbacks['textDocument/peekDefinition'] = function(_, _, result) + if result == nil or vim.tbl_isempty(result) then return end + -- TODO(ashkan) what to do with multiple locations? + result = result[1] + local bufnr = uri_to_bufnr(result.uri) + assert(bufnr) + local start = result.range.start + local finish = result.range["end"] + util.open_floating_peek_preview(bufnr, start, finish, { offset_x = 1 }) + util.open_floating_preview({"*Peek:*", string.rep(" ", finish.character - start.character + 1) }, 'markdown', { offset_y = -(finish.line - start.line) }) +end + +--- Convert SignatureHelp response to preview contents. +-- https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#textDocument_signatureHelp +local function signature_help_to_preview_contents(input) + if not input.signatures then + return + end + --The active signature. If omitted or the value lies outside the range of + --`signatures` the value defaults to zero or is ignored if `signatures.length + --=== 0`. Whenever possible implementors should make an active decision about + --the active signature and shouldn't rely on a default value. + local contents = {} + local active_signature = input.activeSignature or 0 + -- If the activeSignature is not inside the valid range, then clip it. + if active_signature >= #input.signatures then + active_signature = 0 + end + local signature = input.signatures[active_signature + 1] + if not signature then + return + end + vim.list_extend(contents, split_lines(signature.label)) + if signature.documentation then + util.convert_input_to_markdown_lines(signature.documentation, contents) + end + if input.parameters then + local active_parameter = input.activeParameter or 0 + -- If the activeParameter is not inside the valid range, then clip it. + if active_parameter >= #input.parameters then + active_parameter = 0 + end + local parameter = signature.parameters and signature.parameters[active_parameter] + if parameter then + --[=[ + --Represents a parameter of a callable-signature. A parameter can + --have a label and a doc-comment. + interface ParameterInformation { + --The label of this parameter information. + -- + --Either a string or an inclusive start and exclusive end offsets within its containing + --signature label. (see SignatureInformation.label). The offsets are based on a UTF-16 + --string representation as `Position` and `Range` does. + -- + --*Note*: a label of type string should be a substring of its containing signature label. + --Its intended use case is to highlight the parameter label part in the `SignatureInformation.label`. + label: string | [number, number]; + --The human-readable doc-comment of this parameter. Will be shown + --in the UI but can be omitted. + documentation?: string | MarkupContent; + } + --]=] + -- TODO highlight parameter + if parameter.documentation then + util.convert_input_to_markdown_lines(parameter.documentation, contents) + end + end + end + return contents +end + +-- textDocument/signatureHelp +-- https://microsoft.github.io/language-server-protocol/specification#textDocument_signatureHelp +builtin_callbacks['textDocument/signatureHelp'] = function(_, _, result) + if result == nil or vim.tbl_isempty(result) then + return + end + + -- TODO show empty popup when signatures is empty? + if #result.signatures > 0 then + local markdown_lines = signature_help_to_preview_contents(result) + if vim.tbl_isempty(markdown_lines) then + markdown_lines = { 'No signature available' } + end + util.open_floating_preview(markdown_lines, 'markdown') + end +end + +local function update_tagstack() + local bufnr = api.nvim_get_current_buf() + local line = vim.fn.line('.') + local col = vim.fn.col('.') + local tagname = vim.fn.expand('') + local item = { bufnr = bufnr, from = { bufnr, line, col, 0 }, tagname = tagname } + local winid = vim.fn.win_getid() + local tagstack = vim.fn.gettagstack(winid) + + local action + + if tagstack.length == tagstack.curidx then + action = 'r' + tagstack.items[tagstack.curidx] = item + elseif tagstack.length > tagstack.curidx then + action = 'r' + if tagstack.curidx > 1 then + tagstack.items = table.insert(tagstack.items[tagstack.curidx - 1], item) + else + tagstack.items = { item } + end + else + action = 'a' + tagstack.items = { item } + end + + tagstack.curidx = tagstack.curidx + 1 + vim.fn.settagstack(winid, tagstack, action) +end + +local function handle_location(result) + -- We can sometimes get a list of locations, so set the first value as the + -- only value we want to handle + -- TODO(ashkan) was this correct^? We could use location lists. + if result[1] ~= nil then + result = result[1] + end + if result.uri == nil then + api.nvim_err_writeln('[LSP] Could not find a valid location') + return + end + local result_file = vim.uri_to_fname(result.uri) + local bufnr = vim.fn.bufadd(result_file) + update_tagstack() + api.nvim_set_current_buf(bufnr) + local start = result.range.start + api.nvim_win_set_cursor(0, {start.line + 1, start.character}) +end + +local function location_callback(_, method, result) + if result == nil or vim.tbl_isempty(result) then + local _ = log.info() and log.info(method, 'No location found') + return nil + end + handle_location(result) + return true +end + +local location_callbacks = { + -- https://microsoft.github.io/language-server-protocol/specification#textDocument_declaration + 'textDocument/declaration'; + -- https://microsoft.github.io/language-server-protocol/specification#textDocument_definition + 'textDocument/definition'; + -- https://microsoft.github.io/language-server-protocol/specification#textDocument_implementation + 'textDocument/implementation'; + -- https://microsoft.github.io/language-server-protocol/specification#textDocument_typeDefinition + 'textDocument/typeDefinition'; +} + +for _, location_method in ipairs(location_callbacks) do + builtin_callbacks[location_method] = location_callback +end + +local function log_message(_, _, result, client_id) + local message_type = result.type + local message = result.message + local client = vim.lsp.get_client_by_id(client_id) + local client_name = client and client.name or string.format("id=%d", client_id) + if not client then + api.nvim_err_writeln(string.format("LSP[%s] client has shut down after sending the message", client_name)) + end + if message_type == protocol.MessageType.Error then + -- Might want to not use err_writeln, + -- but displaying a message with red highlights or something + api.nvim_err_writeln(string.format("LSP[%s] %s", client_name, message)) + else + local message_type_name = protocol.MessageType[message_type] + api.nvim_out_write(string.format("LSP[%s][%s] %s\n", client_name, message_type_name, message)) + end + return result +end + +builtin_callbacks['window/showMessage'] = log_message +builtin_callbacks['window/logMessage'] = log_message + +-- Add boilerplate error validation and logging for all of these. +for k, fn in pairs(builtin_callbacks) do + builtin_callbacks[k] = function(err, method, params, client_id) + local _ = log.debug() and log.debug('builtin_callback', method, { params = params, client_id = client_id, err = err }) + if err then + error(tostring(err)) + end + return fn(err, method, params, client_id) + end +end + +return builtin_callbacks +-- vim:sw=2 ts=2 et diff --git a/runtime/lua/vim/lsp/log.lua b/runtime/lua/vim/lsp/log.lua new file mode 100644 index 0000000000..974eaae38c --- /dev/null +++ b/runtime/lua/vim/lsp/log.lua @@ -0,0 +1,95 @@ +-- Logger for language client plugin. + +local log = {} + +-- Log level dictionary with reverse lookup as well. +-- +-- Can be used to lookup the number from the name or the name from the number. +-- Levels by name: 'trace', 'debug', 'info', 'warn', 'error' +-- Level numbers begin with 'trace' at 0 +log.levels = { + TRACE = 0; + DEBUG = 1; + INFO = 2; + WARN = 3; + ERROR = 4; + -- FATAL = 4; +} + +-- Default log level is warn. +local current_log_level = log.levels.WARN +local log_date_format = "%FT%H:%M:%SZ%z" + +do + local path_sep = vim.loop.os_uname().sysname == "Windows" and "\\" or "/" + local function path_join(...) + return table.concat(vim.tbl_flatten{...}, path_sep) + end + local logfilename = path_join(vim.fn.stdpath('data'), 'vim-lsp.log') + + --- Return the log filename. + function log.get_filename() + return logfilename + end + + vim.fn.mkdir(vim.fn.stdpath('data'), "p") + local logfile = assert(io.open(logfilename, "a+")) + for level, levelnr in pairs(log.levels) do + -- Also export the log level on the root object. + log[level] = levelnr + -- Set the lowercase name as the main use function. + -- If called without arguments, it will check whether the log level is + -- greater than or equal to this one. When called with arguments, it will + -- log at that level (if applicable, it is checked either way). + -- + -- Recommended usage: + -- ``` + -- local _ = log.warn() and log.warn("123") + -- ``` + -- + -- This way you can avoid string allocations if the log level isn't high enough. + log[level:lower()] = function(...) + local argc = select("#", ...) + if levelnr < current_log_level then return false end + if argc == 0 then return true end + local info = debug.getinfo(2, "Sl") + local fileinfo = string.format("%s:%s", info.short_src, info.currentline) + local parts = { table.concat({"[", level, "]", os.date(log_date_format), "]", fileinfo, "]"}, " ") } + for i = 1, argc do + local arg = select(i, ...) + if arg == nil then + table.insert(parts, "nil") + else + table.insert(parts, vim.inspect(arg, {newline=''})) + end + end + logfile:write(table.concat(parts, '\t'), "\n") + logfile:flush() + end + end + -- Add some space to make it easier to distinguish different neovim runs. + logfile:write("\n") +end + +-- This is put here on purpose after the loop above so that it doesn't +-- interfere with iterating the levels +vim.tbl_add_reverse_lookup(log.levels) + +function log.set_level(level) + if type(level) == 'string' then + current_log_level = assert(log.levels[level:upper()], string.format("Invalid log level: %q", level)) + else + assert(type(level) == 'number', "level must be a number or string") + assert(log.levels[level], string.format("Invalid log level: %d", level)) + current_log_level = level + end +end + +-- Return whether the level is sufficient for logging. +-- @param level number log level +function log.should_log(level) + return level >= current_log_level +end + +return log +-- vim:sw=2 ts=2 et diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua new file mode 100644 index 0000000000..1413a88ce2 --- /dev/null +++ b/runtime/lua/vim/lsp/protocol.lua @@ -0,0 +1,936 @@ +-- Protocol for the Microsoft Language Server Protocol (mslsp) + +local protocol = {} + +local function ifnil(a, b) + if a == nil then return b end + return a +end + + +--[=[ +-- Useful for interfacing with: +-- https://github.com/microsoft/language-server-protocol/blob/gh-pages/_specifications/specification-3-14.md +-- https://github.com/microsoft/language-server-protocol/raw/gh-pages/_specifications/specification-3-14.md +function transform_schema_comments() + nvim.command [[silent! '<,'>g/\/\*\*\|\*\/\|^$/d]] + nvim.command [[silent! '<,'>s/^\(\s*\) \* \=\(.*\)/\1--\2/]] +end +function transform_schema_to_table() + transform_schema_comments() + nvim.command [[silent! '<,'>s/: \S\+//]] + nvim.command [[silent! '<,'>s/export const //]] + nvim.command [[silent! '<,'>s/export namespace \(\S*\)\s*{/protocol.\1 = {/]] + nvim.command [[silent! '<,'>s/namespace \(\S*\)\s*{/protocol.\1 = {/]] +end +--]=] + +local constants = { + DiagnosticSeverity = { + -- Reports an error. + Error = 1; + -- Reports a warning. + Warning = 2; + -- Reports an information. + Information = 3; + -- Reports a hint. + Hint = 4; + }; + + MessageType = { + -- An error message. + Error = 1; + -- A warning message. + Warning = 2; + -- An information message. + Info = 3; + -- A log message. + Log = 4; + }; + + -- The file event type. + FileChangeType = { + -- The file got created. + Created = 1; + -- The file got changed. + Changed = 2; + -- The file got deleted. + Deleted = 3; + }; + + -- The kind of a completion entry. + CompletionItemKind = { + Text = 1; + Method = 2; + Function = 3; + Constructor = 4; + Field = 5; + Variable = 6; + Class = 7; + Interface = 8; + Module = 9; + Property = 10; + Unit = 11; + Value = 12; + Enum = 13; + Keyword = 14; + Snippet = 15; + Color = 16; + File = 17; + Reference = 18; + Folder = 19; + EnumMember = 20; + Constant = 21; + Struct = 22; + Event = 23; + Operator = 24; + TypeParameter = 25; + }; + + -- How a completion was triggered + CompletionTriggerKind = { + -- Completion was triggered by typing an identifier (24x7 code + -- complete), manual invocation (e.g Ctrl+Space) or via API. + Invoked = 1; + -- Completion was triggered by a trigger character specified by + -- the `triggerCharacters` properties of the `CompletionRegistrationOptions`. + TriggerCharacter = 2; + -- Completion was re-triggered as the current completion list is incomplete. + TriggerForIncompleteCompletions = 3; + }; + + -- A document highlight kind. + DocumentHighlightKind = { + -- A textual occurrence. + Text = 1; + -- Read-access of a symbol, like reading a variable. + Read = 2; + -- Write-access of a symbol, like writing to a variable. + Write = 3; + }; + + -- A symbol kind. + SymbolKind = { + File = 1; + Module = 2; + Namespace = 3; + Package = 4; + Class = 5; + Method = 6; + Property = 7; + Field = 8; + Constructor = 9; + Enum = 10; + Interface = 11; + Function = 12; + Variable = 13; + Constant = 14; + String = 15; + Number = 16; + Boolean = 17; + Array = 18; + Object = 19; + Key = 20; + Null = 21; + EnumMember = 22; + Struct = 23; + Event = 24; + Operator = 25; + TypeParameter = 26; + }; + + -- Represents reasons why a text document is saved. + TextDocumentSaveReason = { + -- Manually triggered, e.g. by the user pressing save, by starting debugging, + -- or by an API call. + Manual = 1; + -- Automatic after a delay. + AfterDelay = 2; + -- When the editor lost focus. + FocusOut = 3; + }; + + ErrorCodes = { + -- Defined by JSON RPC + ParseError = -32700; + InvalidRequest = -32600; + MethodNotFound = -32601; + InvalidParams = -32602; + InternalError = -32603; + serverErrorStart = -32099; + serverErrorEnd = -32000; + ServerNotInitialized = -32002; + UnknownErrorCode = -32001; + -- Defined by the protocol. + RequestCancelled = -32800; + ContentModified = -32801; + }; + + -- Describes the content type that a client supports in various + -- result literals like `Hover`, `ParameterInfo` or `CompletionItem`. + -- + -- Please note that `MarkupKinds` must not start with a `$`. This kinds + -- are reserved for internal usage. + MarkupKind = { + -- Plain text is supported as a content format + PlainText = 'plaintext'; + -- Markdown is supported as a content format + Markdown = 'markdown'; + }; + + ResourceOperationKind = { + -- Supports creating new files and folders. + Create = 'create'; + -- Supports renaming existing files and folders. + Rename = 'rename'; + -- Supports deleting existing files and folders. + Delete = 'delete'; + }; + + FailureHandlingKind = { + -- Applying the workspace change is simply aborted if one of the changes provided + -- fails. All operations executed before the failing operation stay executed. + Abort = 'abort'; + -- All operations are executed transactionally. That means they either all + -- succeed or no changes at all are applied to the workspace. + Transactional = 'transactional'; + -- If the workspace edit contains only textual file changes they are executed transactionally. + -- If resource changes (create, rename or delete file) are part of the change the failure + -- handling strategy is abort. + TextOnlyTransactional = 'textOnlyTransactional'; + -- The client tries to undo the operations already executed. But there is no + -- guarantee that this succeeds. + Undo = 'undo'; + }; + + -- Known error codes for an `InitializeError`; + InitializeError = { + -- If the protocol version provided by the client can't be handled by the server. + -- @deprecated This initialize error got replaced by client capabilities. There is + -- no version handshake in version 3.0x + unknownProtocolVersion = 1; + }; + + -- Defines how the host (editor) should sync document changes to the language server. + TextDocumentSyncKind = { + -- Documents should not be synced at all. + None = 0; + -- Documents are synced by always sending the full content + -- of the document. + Full = 1; + -- Documents are synced by sending the full content on open. + -- After that only incremental updates to the document are + -- send. + Incremental = 2; + }; + + WatchKind = { + -- Interested in create events. + Create = 1; + -- Interested in change events + Change = 2; + -- Interested in delete events + Delete = 4; + }; + + -- Defines whether the insert text in a completion item should be interpreted as + -- plain text or a snippet. + InsertTextFormat = { + -- The primary text to be inserted is treated as a plain string. + PlainText = 1; + -- The primary text to be inserted is treated as a snippet. + -- + -- A snippet can define tab stops and placeholders with `$1`, `$2` + -- and `${3:foo};`. `$0` defines the final tab stop, it defaults to + -- the end of the snippet. Placeholders with equal identifiers are linked, + -- that is typing in one will update others too. + Snippet = 2; + }; + + -- A set of predefined code action kinds + CodeActionKind = { + -- Empty kind. + Empty = ''; + -- Base kind for quickfix actions + QuickFix = 'quickfix'; + -- Base kind for refactoring actions + Refactor = 'refactor'; + -- Base kind for refactoring extraction actions + -- + -- Example extract actions: + -- + -- - Extract method + -- - Extract function + -- - Extract variable + -- - Extract interface from class + -- - ... + RefactorExtract = 'refactor.extract'; + -- Base kind for refactoring inline actions + -- + -- Example inline actions: + -- + -- - Inline function + -- - Inline variable + -- - Inline constant + -- - ... + RefactorInline = 'refactor.inline'; + -- Base kind for refactoring rewrite actions + -- + -- Example rewrite actions: + -- + -- - Convert JavaScript function to class + -- - Add or remove parameter + -- - Encapsulate field + -- - Make method static + -- - Move method to base class + -- - ... + RefactorRewrite = 'refactor.rewrite'; + -- Base kind for source actions + -- + -- Source code actions apply to the entire file. + Source = 'source'; + -- Base kind for an organize imports source action + SourceOrganizeImports = 'source.organizeImports'; + }; +} + +for k, v in pairs(constants) do + vim.tbl_add_reverse_lookup(v) + protocol[k] = v +end + +--[=[ +--Text document specific client capabilities. +export interface TextDocumentClientCapabilities { + synchronization?: { + --Whether text document synchronization supports dynamic registration. + dynamicRegistration?: boolean; + --The client supports sending will save notifications. + willSave?: boolean; + --The client supports sending a will save request and + --waits for a response providing text edits which will + --be applied to the document before it is saved. + willSaveWaitUntil?: boolean; + --The client supports did save notifications. + didSave?: boolean; + } + --Capabilities specific to the `textDocument/completion` + completion?: { + --Whether completion supports dynamic registration. + dynamicRegistration?: boolean; + --The client supports the following `CompletionItem` specific + --capabilities. + completionItem?: { + --The client supports snippets as insert text. + -- + --A snippet can define tab stops and placeholders with `$1`, `$2` + --and `${3:foo}`. `$0` defines the final tab stop, it defaults to + --the end of the snippet. Placeholders with equal identifiers are linked, + --that is typing in one will update others too. + snippetSupport?: boolean; + --The client supports commit characters on a completion item. + commitCharactersSupport?: boolean + --The client supports the following content formats for the documentation + --property. The order describes the preferred format of the client. + documentationFormat?: MarkupKind[]; + --The client supports the deprecated property on a completion item. + deprecatedSupport?: boolean; + --The client supports the preselect property on a completion item. + preselectSupport?: boolean; + } + completionItemKind?: { + --The completion item kind values the client supports. When this + --property exists the client also guarantees that it will + --handle values outside its set gracefully and falls back + --to a default value when unknown. + -- + --If this property is not present the client only supports + --the completion items kinds from `Text` to `Reference` as defined in + --the initial version of the protocol. + valueSet?: CompletionItemKind[]; + }, + --The client supports to send additional context information for a + --`textDocument/completion` request. + contextSupport?: boolean; + }; + --Capabilities specific to the `textDocument/hover` + hover?: { + --Whether hover supports dynamic registration. + dynamicRegistration?: boolean; + --The client supports the follow content formats for the content + --property. The order describes the preferred format of the client. + contentFormat?: MarkupKind[]; + }; + --Capabilities specific to the `textDocument/signatureHelp` + signatureHelp?: { + --Whether signature help supports dynamic registration. + dynamicRegistration?: boolean; + --The client supports the following `SignatureInformation` + --specific properties. + signatureInformation?: { + --The client supports the follow content formats for the documentation + --property. The order describes the preferred format of the client. + documentationFormat?: MarkupKind[]; + --Client capabilities specific to parameter information. + parameterInformation?: { + --The client supports processing label offsets instead of a + --simple label string. + -- + --Since 3.14.0 + labelOffsetSupport?: boolean; + } + }; + }; + --Capabilities specific to the `textDocument/references` + references?: { + --Whether references supports dynamic registration. + dynamicRegistration?: boolean; + }; + --Capabilities specific to the `textDocument/documentHighlight` + documentHighlight?: { + --Whether document highlight supports dynamic registration. + dynamicRegistration?: boolean; + }; + --Capabilities specific to the `textDocument/documentSymbol` + documentSymbol?: { + --Whether document symbol supports dynamic registration. + dynamicRegistration?: boolean; + --Specific capabilities for the `SymbolKind`. + symbolKind?: { + --The symbol kind values the client supports. When this + --property exists the client also guarantees that it will + --handle values outside its set gracefully and falls back + --to a default value when unknown. + -- + --If this property is not present the client only supports + --the symbol kinds from `File` to `Array` as defined in + --the initial version of the protocol. + valueSet?: SymbolKind[]; + } + --The client supports hierarchical document symbols. + hierarchicalDocumentSymbolSupport?: boolean; + }; + --Capabilities specific to the `textDocument/formatting` + formatting?: { + --Whether formatting supports dynamic registration. + dynamicRegistration?: boolean; + }; + --Capabilities specific to the `textDocument/rangeFormatting` + rangeFormatting?: { + --Whether range formatting supports dynamic registration. + dynamicRegistration?: boolean; + }; + --Capabilities specific to the `textDocument/onTypeFormatting` + onTypeFormatting?: { + --Whether on type formatting supports dynamic registration. + dynamicRegistration?: boolean; + }; + --Capabilities specific to the `textDocument/declaration` + declaration?: { + --Whether declaration supports dynamic registration. If this is set to `true` + --the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)` + --return value for the corresponding server capability as well. + dynamicRegistration?: boolean; + --The client supports additional metadata in the form of declaration links. + -- + --Since 3.14.0 + linkSupport?: boolean; + }; + --Capabilities specific to the `textDocument/definition`. + -- + --Since 3.14.0 + definition?: { + --Whether definition supports dynamic registration. + dynamicRegistration?: boolean; + --The client supports additional metadata in the form of definition links. + linkSupport?: boolean; + }; + --Capabilities specific to the `textDocument/typeDefinition` + -- + --Since 3.6.0 + typeDefinition?: { + --Whether typeDefinition supports dynamic registration. If this is set to `true` + --the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)` + --return value for the corresponding server capability as well. + dynamicRegistration?: boolean; + --The client supports additional metadata in the form of definition links. + -- + --Since 3.14.0 + linkSupport?: boolean; + }; + --Capabilities specific to the `textDocument/implementation`. + -- + --Since 3.6.0 + implementation?: { + --Whether implementation supports dynamic registration. If this is set to `true` + --the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)` + --return value for the corresponding server capability as well. + dynamicRegistration?: boolean; + --The client supports additional metadata in the form of definition links. + -- + --Since 3.14.0 + linkSupport?: boolean; + }; + --Capabilities specific to the `textDocument/codeAction` + codeAction?: { + --Whether code action supports dynamic registration. + dynamicRegistration?: boolean; + --The client support code action literals as a valid + --response of the `textDocument/codeAction` request. + -- + --Since 3.8.0 + codeActionLiteralSupport?: { + --The code action kind is support with the following value + --set. + codeActionKind: { + --The code action kind values the client supports. When this + --property exists the client also guarantees that it will + --handle values outside its set gracefully and falls back + --to a default value when unknown. + valueSet: CodeActionKind[]; + }; + }; + }; + --Capabilities specific to the `textDocument/codeLens` + codeLens?: { + --Whether code lens supports dynamic registration. + dynamicRegistration?: boolean; + }; + --Capabilities specific to the `textDocument/documentLink` + documentLink?: { + --Whether document link supports dynamic registration. + dynamicRegistration?: boolean; + }; + --Capabilities specific to the `textDocument/documentColor` and the + --`textDocument/colorPresentation` request. + -- + --Since 3.6.0 + colorProvider?: { + --Whether colorProvider supports dynamic registration. If this is set to `true` + --the client supports the new `(ColorProviderOptions & TextDocumentRegistrationOptions & StaticRegistrationOptions)` + --return value for the corresponding server capability as well. + dynamicRegistration?: boolean; + } + --Capabilities specific to the `textDocument/rename` + rename?: { + --Whether rename supports dynamic registration. + dynamicRegistration?: boolean; + --The client supports testing for validity of rename operations + --before execution. + prepareSupport?: boolean; + }; + --Capabilities specific to `textDocument/publishDiagnostics`. + publishDiagnostics?: { + --Whether the clients accepts diagnostics with related information. + relatedInformation?: boolean; + }; + --Capabilities specific to `textDocument/foldingRange` requests. + -- + --Since 3.10.0 + foldingRange?: { + --Whether implementation supports dynamic registration for folding range providers. If this is set to `true` + --the client supports the new `(FoldingRangeProviderOptions & TextDocumentRegistrationOptions & StaticRegistrationOptions)` + --return value for the corresponding server capability as well. + dynamicRegistration?: boolean; + --The maximum number of folding ranges that the client prefers to receive per document. The value serves as a + --hint, servers are free to follow the limit. + rangeLimit?: number; + --If set, the client signals that it only supports folding complete lines. If set, client will + --ignore specified `startCharacter` and `endCharacter` properties in a FoldingRange. + lineFoldingOnly?: boolean; + }; +} +--]=] + +--[=[ +--Workspace specific client capabilities. +export interface WorkspaceClientCapabilities { + --The client supports applying batch edits to the workspace by supporting + --the request 'workspace/applyEdit' + applyEdit?: boolean; + --Capabilities specific to `WorkspaceEdit`s + workspaceEdit?: { + --The client supports versioned document changes in `WorkspaceEdit`s + documentChanges?: boolean; + --The resource operations the client supports. Clients should at least + --support 'create', 'rename' and 'delete' files and folders. + resourceOperations?: ResourceOperationKind[]; + --The failure handling strategy of a client if applying the workspace edit + --fails. + failureHandling?: FailureHandlingKind; + }; + --Capabilities specific to the `workspace/didChangeConfiguration` notification. + didChangeConfiguration?: { + --Did change configuration notification supports dynamic registration. + dynamicRegistration?: boolean; + }; + --Capabilities specific to the `workspace/didChangeWatchedFiles` notification. + didChangeWatchedFiles?: { + --Did change watched files notification supports dynamic registration. Please note + --that the current protocol doesn't support static configuration for file changes + --from the server side. + dynamicRegistration?: boolean; + }; + --Capabilities specific to the `workspace/symbol` request. + symbol?: { + --Symbol request supports dynamic registration. + dynamicRegistration?: boolean; + --Specific capabilities for the `SymbolKind` in the `workspace/symbol` request. + symbolKind?: { + --The symbol kind values the client supports. When this + --property exists the client also guarantees that it will + --handle values outside its set gracefully and falls back + --to a default value when unknown. + -- + --If this property is not present the client only supports + --the symbol kinds from `File` to `Array` as defined in + --the initial version of the protocol. + valueSet?: SymbolKind[]; + } + }; + --Capabilities specific to the `workspace/executeCommand` request. + executeCommand?: { + --Execute command supports dynamic registration. + dynamicRegistration?: boolean; + }; + --The client has support for workspace folders. + -- + --Since 3.6.0 + workspaceFolders?: boolean; + --The client supports `workspace/configuration` requests. + -- + --Since 3.6.0 + configuration?: boolean; +} +--]=] + +function protocol.make_client_capabilities() + return { + textDocument = { + synchronization = { + dynamicRegistration = false; + + -- TODO(ashkan) Send textDocument/willSave before saving (BufWritePre) + willSave = false; + + -- TODO(ashkan) Implement textDocument/willSaveWaitUntil + willSaveWaitUntil = false; + + -- Send textDocument/didSave after saving (BufWritePost) + didSave = true; + }; + completion = { + dynamicRegistration = false; + completionItem = { + + -- TODO(tjdevries): Is it possible to implement this in plain lua? + snippetSupport = false; + commitCharactersSupport = false; + preselectSupport = false; + deprecatedSupport = false; + documentationFormat = { protocol.MarkupKind.Markdown; protocol.MarkupKind.PlainText }; + }; + completionItemKind = { + valueSet = (function() + local res = {} + for k in pairs(protocol.CompletionItemKind) do + if type(k) == 'number' then table.insert(res, k) end + end + return res + end)(); + }; + + -- TODO(tjdevries): Implement this + contextSupport = false; + }; + hover = { + dynamicRegistration = false; + contentFormat = { protocol.MarkupKind.Markdown; protocol.MarkupKind.PlainText }; + }; + signatureHelp = { + dynamicRegistration = false; + signatureInformation = { + documentationFormat = { protocol.MarkupKind.Markdown; protocol.MarkupKind.PlainText }; + -- parameterInformation = { + -- labelOffsetSupport = false; + -- }; + }; + }; + references = { + dynamicRegistration = false; + }; + documentHighlight = { + dynamicRegistration = false + }; + -- documentSymbol = { + -- dynamicRegistration = false; + -- symbolKind = { + -- valueSet = (function() + -- local res = {} + -- for k in pairs(protocol.SymbolKind) do + -- if type(k) == 'string' then table.insert(res, k) end + -- end + -- return res + -- end)(); + -- }; + -- hierarchicalDocumentSymbolSupport = false; + -- }; + }; + workspace = nil; + experimental = nil; + } +end + +function protocol.make_text_document_position_params() + local position = vim.api.nvim_win_get_cursor(0) + return { + textDocument = { + uri = vim.uri_from_bufnr() + }; + position = { + line = position[1] - 1; + character = position[2]; + } + } +end + +--[=[ +export interface DocumentFilter { + --A language id, like `typescript`. + language?: string; + --A Uri [scheme](#Uri.scheme), like `file` or `untitled`. + scheme?: string; + --A glob pattern, like `*.{ts,js}`. + -- + --Glob patterns can have the following syntax: + --- `*` to match one or more characters in a path segment + --- `?` to match on one character in a path segment + --- `**` to match any number of path segments, including none + --- `{}` to group conditions (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files) + --- `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …) + --- `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`) + pattern?: string; +} +--]=] + +--[[ +--Static registration options to be returned in the initialize request. +interface StaticRegistrationOptions { + --The id used to register the request. The id can be used to deregister + --the request again. See also Registration#id. + id?: string; +} + +export interface DocumentFilter { + --A language id, like `typescript`. + language?: string; + --A Uri [scheme](#Uri.scheme), like `file` or `untitled`. + scheme?: string; + --A glob pattern, like `*.{ts,js}`. + -- + --Glob patterns can have the following syntax: + --- `*` to match one or more characters in a path segment + --- `?` to match on one character in a path segment + --- `**` to match any number of path segments, including none + --- `{}` to group conditions (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files) + --- `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …) + --- `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`) + pattern?: string; +} +export type DocumentSelector = DocumentFilter[]; +export interface TextDocumentRegistrationOptions { + --A document selector to identify the scope of the registration. If set to null + --the document selector provided on the client side will be used. + documentSelector: DocumentSelector | null; +} + +--Code Action options. +export interface CodeActionOptions { + --CodeActionKinds that this server may return. + -- + --The list of kinds may be generic, such as `CodeActionKind.Refactor`, or the server + --may list out every specific kind they provide. + codeActionKinds?: CodeActionKind[]; +} + +interface ServerCapabilities { + --Defines how text documents are synced. Is either a detailed structure defining each notification or + --for backwards compatibility the TextDocumentSyncKind number. If omitted it defaults to `TextDocumentSyncKind.None`. + textDocumentSync?: TextDocumentSyncOptions | number; + --The server provides hover support. + hoverProvider?: boolean; + --The server provides completion support. + completionProvider?: CompletionOptions; + --The server provides signature help support. + signatureHelpProvider?: SignatureHelpOptions; + --The server provides goto definition support. + definitionProvider?: boolean; + --The server provides Goto Type Definition support. + -- + --Since 3.6.0 + typeDefinitionProvider?: boolean | (TextDocumentRegistrationOptions & StaticRegistrationOptions); + --The server provides Goto Implementation support. + -- + --Since 3.6.0 + implementationProvider?: boolean | (TextDocumentRegistrationOptions & StaticRegistrationOptions); + --The server provides find references support. + referencesProvider?: boolean; + --The server provides document highlight support. + documentHighlightProvider?: boolean; + --The server provides document symbol support. + documentSymbolProvider?: boolean; + --The server provides workspace symbol support. + workspaceSymbolProvider?: boolean; + --The server provides code actions. The `CodeActionOptions` return type is only + --valid if the client signals code action literal support via the property + --`textDocument.codeAction.codeActionLiteralSupport`. + codeActionProvider?: boolean | CodeActionOptions; + --The server provides code lens. + codeLensProvider?: CodeLensOptions; + --The server provides document formatting. + documentFormattingProvider?: boolean; + --The server provides document range formatting. + documentRangeFormattingProvider?: boolean; + --The server provides document formatting on typing. + documentOnTypeFormattingProvider?: DocumentOnTypeFormattingOptions; + --The server provides rename support. RenameOptions may only be + --specified if the client states that it supports + --`prepareSupport` in its initial `initialize` request. + renameProvider?: boolean | RenameOptions; + --The server provides document link support. + documentLinkProvider?: DocumentLinkOptions; + --The server provides color provider support. + -- + --Since 3.6.0 + colorProvider?: boolean | ColorProviderOptions | (ColorProviderOptions & TextDocumentRegistrationOptions & StaticRegistrationOptions); + --The server provides folding provider support. + -- + --Since 3.10.0 + foldingRangeProvider?: boolean | FoldingRangeProviderOptions | (FoldingRangeProviderOptions & TextDocumentRegistrationOptions & StaticRegistrationOptions); + --The server provides go to declaration support. + -- + --Since 3.14.0 + declarationProvider?: boolean | (TextDocumentRegistrationOptions & StaticRegistrationOptions); + --The server provides execute command support. + executeCommandProvider?: ExecuteCommandOptions; + --Workspace specific server capabilities + workspace?: { + --The server supports workspace folder. + -- + --Since 3.6.0 + workspaceFolders?: { + * The server has support for workspace folders + supported?: boolean; + * Whether the server wants to receive workspace folder + * change notifications. + * + * If a strings is provided the string is treated as a ID + * under which the notification is registered on the client + * side. The ID can be used to unregister for these events + * using the `client/unregisterCapability` request. + changeNotifications?: string | boolean; + } + } + --Experimental server capabilities. + experimental?: any; +} +--]] +function protocol.resolve_capabilities(server_capabilities) + local general_properties = {} + local text_document_sync_properties + do + local TextDocumentSyncKind = protocol.TextDocumentSyncKind + local textDocumentSync = server_capabilities.textDocumentSync + if textDocumentSync == nil then + -- Defaults if omitted. + text_document_sync_properties = { + text_document_open_close = false; + text_document_did_change = TextDocumentSyncKind.None; +-- text_document_did_change = false; + text_document_will_save = false; + text_document_will_save_wait_until = false; + text_document_save = false; + text_document_save_include_text = false; + } + elseif type(textDocumentSync) == 'number' then + -- Backwards compatibility + if not TextDocumentSyncKind[textDocumentSync] then + return nil, "Invalid server TextDocumentSyncKind for textDocumentSync" + end + text_document_sync_properties = { + text_document_open_close = true; + text_document_did_change = textDocumentSync; + text_document_will_save = false; + text_document_will_save_wait_until = false; + text_document_save = false; + text_document_save_include_text = false; + } + elseif type(textDocumentSync) == 'table' then + text_document_sync_properties = { + text_document_open_close = ifnil(textDocumentSync.openClose, false); + text_document_did_change = ifnil(textDocumentSync.change, TextDocumentSyncKind.None); + text_document_will_save = ifnil(textDocumentSync.willSave, false); + text_document_will_save_wait_until = ifnil(textDocumentSync.willSaveWaitUntil, false); + text_document_save = ifnil(textDocumentSync.save, false); + text_document_save_include_text = ifnil(textDocumentSync.save and textDocumentSync.save.includeText, false); + } + else + return nil, string.format("Invalid type for textDocumentSync: %q", type(textDocumentSync)) + end + end + general_properties.hover = server_capabilities.hoverProvider or false + general_properties.goto_definition = server_capabilities.definitionProvider or false + general_properties.find_references = server_capabilities.referencesProvider or false + general_properties.document_highlight = server_capabilities.documentHighlightProvider or false + general_properties.document_symbol = server_capabilities.documentSymbolProvider or false + general_properties.workspace_symbol = server_capabilities.workspaceSymbolProvider or false + general_properties.document_formatting = server_capabilities.documentFormattingProvider or false + general_properties.document_range_formatting = server_capabilities.documentRangeFormattingProvider or false + + if server_capabilities.codeActionProvider == nil then + general_properties.code_action = false + elseif type(server_capabilities.codeActionProvider) == 'boolean' then + general_properties.code_action = server_capabilities.codeActionProvider + elseif type(server_capabilities.codeActionProvider) == 'table' then + -- TODO(ashkan) support CodeActionKind + general_properties.code_action = false + else + error("The server sent invalid codeActionProvider") + end + + if server_capabilities.implementationProvider == nil then + general_properties.implementation = false + elseif type(server_capabilities.implementationProvider) == 'boolean' then + general_properties.implementation = server_capabilities.implementationProvider + elseif type(server_capabilities.implementationProvider) == 'table' then + -- TODO(ashkan) support more detailed implementation options. + general_properties.implementation = false + else + error("The server sent invalid implementationProvider") + end + + local signature_help_properties + if server_capabilities.signatureHelpProvider == nil then + signature_help_properties = { + signature_help = false; + signature_help_trigger_characters = {}; + } + elseif type(server_capabilities.signatureHelpProvider) == 'table' then + signature_help_properties = { + signature_help = true; + -- The characters that trigger signature help automatically. + signature_help_trigger_characters = server_capabilities.signatureHelpProvider.triggerCharacters or {}; + } + else + error("The server sent invalid signatureHelpProvider") + end + + return vim.tbl_extend("error" + , text_document_sync_properties + , signature_help_properties + , general_properties + ) +end + +return protocol +-- vim:sw=2 ts=2 et diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua new file mode 100644 index 0000000000..e0ec8863d6 --- /dev/null +++ b/runtime/lua/vim/lsp/rpc.lua @@ -0,0 +1,451 @@ +local uv = vim.loop +local log = require('vim.lsp.log') +local protocol = require('vim.lsp.protocol') +local validate, schedule, schedule_wrap = vim.validate, vim.schedule, vim.schedule_wrap + +-- TODO replace with a better implementation. +local function json_encode(data) + local status, result = pcall(vim.fn.json_encode, data) + if status then + return result + else + return nil, result + end +end +local function json_decode(data) + local status, result = pcall(vim.fn.json_decode, data) + if status then + return result + else + return nil, result + end +end + +local function is_dir(filename) + local stat = vim.loop.fs_stat(filename) + return stat and stat.type == 'directory' or false +end + +local NIL = vim.NIL +local function convert_NIL(v) + if v == NIL then return nil end + return v +end + +-- If a dictionary is passed in, turn it into a list of string of "k=v" +-- Accepts a table which can be composed of k=v strings or map-like +-- specification, such as: +-- +-- ``` +-- { +-- "PRODUCTION=false"; +-- "PATH=/usr/bin/"; +-- PORT = 123; +-- HOST = "0.0.0.0"; +-- } +-- ``` +-- +-- Non-string values will be cast with `tostring` +local function force_env_list(final_env) + if final_env then + local env = final_env + final_env = {} + for k,v in pairs(env) do + -- If it's passed in as a dict, then convert to list of "k=v" + if type(k) == "string" then + table.insert(final_env, k..'='..tostring(v)) + elseif type(v) == 'string' then + table.insert(final_env, v) + else + -- TODO is this right or should I exception here? + -- Try to coerce other values to string. + table.insert(final_env, tostring(v)) + end + end + return final_env + end +end + +local function format_message_with_content_length(encoded_message) + return table.concat { + 'Content-Length: '; tostring(#encoded_message); '\r\n\r\n'; + encoded_message; + } +end + +--- Parse an LSP Message's header +-- @param header: The header to parse. +local function parse_headers(header) + if type(header) ~= 'string' then + return nil + end + local headers = {} + for line in vim.gsplit(header, '\r\n', true) do + if line == '' then + break + end + local key, value = line:match("^%s*(%S+)%s*:%s*(.+)%s*$") + if key then + key = key:lower():gsub('%-', '_') + headers[key] = value + else + local _ = log.error() and log.error("invalid header line %q", line) + error(string.format("invalid header line %q", line)) + end + end + headers.content_length = tonumber(headers.content_length) + or error(string.format("Content-Length not found in headers. %q", header)) + return headers +end + +-- This is the start of any possible header patterns. The gsub converts it to a +-- case insensitive pattern. +local header_start_pattern = ("content"):gsub("%w", function(c) return "["..c..c:upper().."]" end) + +local function request_parser_loop() + local buffer = '' + while true do + -- A message can only be complete if it has a double CRLF and also the full + -- payload, so first let's check for the CRLFs + local start, finish = buffer:find('\r\n\r\n', 1, true) + -- Start parsing the headers + if start then + -- This is a workaround for servers sending initial garbage before + -- sending headers, such as if a bash script sends stdout. It assumes + -- that we know all of the headers ahead of time. At this moment, the + -- only valid headers start with "Content-*", so that's the thing we will + -- be searching for. + -- TODO(ashkan) I'd like to remove this, but it seems permanent :( + local buffer_start = buffer:find(header_start_pattern) + local headers = parse_headers(buffer:sub(buffer_start, start-1)) + buffer = buffer:sub(finish+1) + local content_length = headers.content_length + -- Keep waiting for data until we have enough. + while #buffer < content_length do + buffer = buffer..(coroutine.yield() + or error("Expected more data for the body. The server may have died.")) -- TODO hmm. + end + local body = buffer:sub(1, content_length) + buffer = buffer:sub(content_length + 1) + -- Yield our data. + buffer = buffer..(coroutine.yield(headers, body) + or error("Expected more data for the body. The server may have died.")) -- TODO hmm. + else + -- Get more data since we don't have enough. + buffer = buffer..(coroutine.yield() + or error("Expected more data for the header. The server may have died.")) -- TODO hmm. + end + end +end + +local client_errors = vim.tbl_add_reverse_lookup { + INVALID_SERVER_MESSAGE = 1; + INVALID_SERVER_JSON = 2; + NO_RESULT_CALLBACK_FOUND = 3; + READ_ERROR = 4; + NOTIFICATION_HANDLER_ERROR = 5; + SERVER_REQUEST_HANDLER_ERROR = 6; + SERVER_RESULT_CALLBACK_ERROR = 7; +} + +local function format_rpc_error(err) + validate { + err = { err, 't' }; + } + local code_name = assert(protocol.ErrorCodes[err.code], "err.code is invalid") + local message_parts = {"RPC", code_name} + if err.message then + table.insert(message_parts, "message = ") + table.insert(message_parts, string.format("%q", err.message)) + end + if err.data then + table.insert(message_parts, "data = ") + table.insert(message_parts, vim.inspect(err.data)) + end + return table.concat(message_parts, ' ') +end + +local function rpc_response_error(code, message, data) + -- TODO should this error or just pick a sane error (like InternalError)? + local code_name = assert(protocol.ErrorCodes[code], 'Invalid rpc error code') + return setmetatable({ + code = code; + message = message or code_name; + data = data; + }, { + __tostring = format_rpc_error; + }) +end + +local default_handlers = {} +function default_handlers.notification(method, params) + local _ = log.debug() and log.debug('notification', method, params) +end +function default_handlers.server_request(method, params) + local _ = log.debug() and log.debug('server_request', method, params) + return nil, rpc_response_error(protocol.ErrorCodes.MethodNotFound) +end +function default_handlers.on_exit(code, signal) + local _ = log.info() and log.info("client exit", { code = code, signal = signal }) +end +function default_handlers.on_error(code, err) + local _ = log.error() and log.error('client_error:', client_errors[code], err) +end + +--- Create and start an RPC client. +-- @param cmd [ +local function create_and_start_client(cmd, cmd_args, handlers, extra_spawn_params) + local _ = log.info() and log.info("Starting RPC client", {cmd = cmd, args = cmd_args, extra = extra_spawn_params}) + validate { + cmd = { cmd, 's' }; + cmd_args = { cmd_args, 't' }; + handlers = { handlers, 't', true }; + } + + if not (vim.fn.executable(cmd) == 1) then + error(string.format("The given command %q is not executable.", cmd)) + end + if handlers then + local user_handlers = handlers + handlers = {} + for handle_name, default_handler in pairs(default_handlers) do + local user_handler = user_handlers[handle_name] + if user_handler then + if type(user_handler) ~= 'function' then + error(string.format("handler.%s must be a function", handle_name)) + end + -- server_request is wrapped elsewhere. + if not (handle_name == 'server_request' + or handle_name == 'on_exit') -- TODO this blocks the loop exiting for some reason. + then + user_handler = schedule_wrap(user_handler) + end + handlers[handle_name] = user_handler + else + handlers[handle_name] = default_handler + end + end + else + handlers = default_handlers + end + + local stdin = uv.new_pipe(false) + local stdout = uv.new_pipe(false) + local stderr = uv.new_pipe(false) + + local message_index = 0 + local message_callbacks = {} + + local handle, pid + do + local function onexit(code, signal) + stdin:close() + stdout:close() + stderr:close() + handle:close() + -- Make sure that message_callbacks can be gc'd. + message_callbacks = nil + handlers.on_exit(code, signal) + end + local spawn_params = { + args = cmd_args; + stdio = {stdin, stdout, stderr}; + } + if extra_spawn_params then + spawn_params.cwd = extra_spawn_params.cwd + if spawn_params.cwd then + assert(is_dir(spawn_params.cwd), "cwd must be a directory") + end + spawn_params.env = force_env_list(extra_spawn_params.env) + end + handle, pid = uv.spawn(cmd, spawn_params, onexit) + end + + local function encode_and_send(payload) + local _ = log.debug() and log.debug("rpc.send.payload", payload) + if handle:is_closing() then return false end + -- TODO(ashkan) remove this once we have a Lua json_encode + schedule(function() + local encoded = assert(json_encode(payload)) + stdin:write(format_message_with_content_length(encoded)) + end) + return true + end + + local function send_notification(method, params) + local _ = log.debug() and log.debug("rpc.notify", method, params) + return encode_and_send { + jsonrpc = "2.0"; + method = method; + params = params; + } + end + + local function send_response(request_id, err, result) + return encode_and_send { + id = request_id; + jsonrpc = "2.0"; + error = err; + result = result; + } + end + + local function send_request(method, params, callback) + validate { + callback = { callback, 'f' }; + } + message_index = message_index + 1 + local message_id = message_index + local result = encode_and_send { + id = message_id; + jsonrpc = "2.0"; + method = method; + params = params; + } + if result then + message_callbacks[message_id] = schedule_wrap(callback) + return result, message_id + else + return false + end + end + + stderr:read_start(function(_err, chunk) + if chunk then + local _ = log.error() and log.error("rpc", cmd, "stderr", chunk) + end + end) + + local function on_error(errkind, ...) + assert(client_errors[errkind]) + -- TODO what to do if this fails? + pcall(handlers.on_error, errkind, ...) + end + local function pcall_handler(errkind, status, head, ...) + if not status then + on_error(errkind, head, ...) + return status, head + end + return status, head, ... + end + local function try_call(errkind, fn, ...) + return pcall_handler(errkind, pcall(fn, ...)) + end + + -- TODO periodically check message_callbacks for old requests past a certain + -- time and log them. This would require storing the timestamp. I could call + -- them with an error then, perhaps. + + local function handle_body(body) + local decoded, err = json_decode(body) + if not decoded then + on_error(client_errors.INVALID_SERVER_JSON, err) + end + local _ = log.debug() and log.debug("decoded", decoded) + + if type(decoded.method) == 'string' and decoded.id then + -- Server Request + decoded.params = convert_NIL(decoded.params) + -- Schedule here so that the users functions don't trigger an error and + -- we can still use the result. + schedule(function() + local status, result + status, result, err = try_call(client_errors.SERVER_REQUEST_HANDLER_ERROR, + handlers.server_request, decoded.method, decoded.params) + local _ = log.debug() and log.debug("server_request: callback result", { status = status, result = result, err = err }) + if status then + if not (result or err) then + -- TODO this can be a problem if `null` is sent for result. needs vim.NIL + error(string.format("method %q: either a result or an error must be sent to the server in response", decoded.method)) + end + if err then + assert(type(err) == 'table', "err must be a table. Use rpc_response_error to help format errors.") + local code_name = assert(protocol.ErrorCodes[err.code], "Errors must use protocol.ErrorCodes. Use rpc_response_error to help format errors.") + err.message = err.message or code_name + end + else + -- On an exception, result will contain the error message. + err = rpc_response_error(protocol.ErrorCodes.InternalError, result) + result = nil + end + send_response(decoded.id, err, result) + end) + -- This works because we are expecting vim.NIL here + elseif decoded.id and (decoded.result or decoded.error) then + -- Server Result + decoded.error = convert_NIL(decoded.error) + decoded.result = convert_NIL(decoded.result) + + -- We sent a number, so we expect a number. + local result_id = tonumber(decoded.id) + local callback = message_callbacks[result_id] + if callback then + message_callbacks[result_id] = nil + validate { + callback = { callback, 'f' }; + } + if decoded.error then + decoded.error = setmetatable(decoded.error, { + __tostring = format_rpc_error; + }) + end + try_call(client_errors.SERVER_RESULT_CALLBACK_ERROR, + callback, decoded.error, decoded.result) + else + on_error(client_errors.NO_RESULT_CALLBACK_FOUND, decoded) + local _ = log.error() and log.error("No callback found for server response id "..result_id) + end + elseif type(decoded.method) == 'string' then + -- Notification + decoded.params = convert_NIL(decoded.params) + try_call(client_errors.NOTIFICATION_HANDLER_ERROR, + handlers.notification, decoded.method, decoded.params) + else + -- Invalid server message + on_error(client_errors.INVALID_SERVER_MESSAGE, decoded) + end + end + -- TODO(ashkan) remove this once we have a Lua json_decode + handle_body = schedule_wrap(handle_body) + + local request_parser = coroutine.wrap(request_parser_loop) + request_parser() + stdout:read_start(function(err, chunk) + if err then + -- TODO better handling. Can these be intermittent errors? + on_error(client_errors.READ_ERROR, err) + return + end + -- This should signal that we are done reading from the client. + if not chunk then return end + -- Flush anything in the parser by looping until we don't get a result + -- anymore. + while true do + local headers, body = request_parser(chunk) + -- If we successfully parsed, then handle the response. + if headers then + handle_body(body) + -- Set chunk to empty so that we can call request_parser to get + -- anything existing in the parser to flush. + chunk = '' + else + break + end + end + end) + + return { + pid = pid; + handle = handle; + request = send_request; + notify = send_notification; + } +end + +return { + start = create_and_start_client; + rpc_response_error = rpc_response_error; + format_rpc_error = format_rpc_error; + client_errors = client_errors; +} +-- vim:sw=2 ts=2 et diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua new file mode 100644 index 0000000000..f96e0f01a8 --- /dev/null +++ b/runtime/lua/vim/lsp/util.lua @@ -0,0 +1,557 @@ +local protocol = require 'vim.lsp.protocol' +local validate = vim.validate +local api = vim.api + +local M = {} + +local split = vim.split +local function split_lines(value) + return split(value, '\n', true) +end + +local list_extend = vim.list_extend + +--- Find the longest shared prefix between prefix and word. +-- e.g. remove_prefix("123tes", "testing") == "ting" +local function remove_prefix(prefix, word) + local max_prefix_length = math.min(#prefix, #word) + local prefix_length = 0 + for i = 1, max_prefix_length do + local current_line_suffix = prefix:sub(-i) + local word_prefix = word:sub(1, i) + if current_line_suffix == word_prefix then + prefix_length = i + end + end + return word:sub(prefix_length + 1) +end + +local function resolve_bufnr(bufnr) + if bufnr == nil or bufnr == 0 then + return api.nvim_get_current_buf() + end + return bufnr +end + +-- local valid_windows_path_characters = "[^<>:\"/\\|?*]" +-- local valid_unix_path_characters = "[^/]" +-- https://github.com/davidm/lua-glob-pattern +-- https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names +-- function M.glob_to_regex(glob) +-- end + +--- Apply the TextEdit response. +-- @params TextEdit [table] see https://microsoft.github.io/language-server-protocol/specification +function M.text_document_apply_text_edit(text_edit, bufnr) + bufnr = resolve_bufnr(bufnr) + local range = text_edit.range + local start = range.start + local finish = range['end'] + local new_lines = split_lines(text_edit.newText) + if start.character == 0 and finish.character == 0 then + api.nvim_buf_set_lines(bufnr, start.line, finish.line, false, new_lines) + return + end + api.nvim_err_writeln('apply_text_edit currently only supports character ranges starting at 0') + error('apply_text_edit currently only supports character ranges starting at 0') + return + -- TODO test and finish this support for character ranges. +-- local lines = api.nvim_buf_get_lines(0, start.line, finish.line + 1, false) +-- local suffix = lines[#lines]:sub(finish.character+2) +-- local prefix = lines[1]:sub(start.character+2) +-- new_lines[#new_lines] = new_lines[#new_lines]..suffix +-- new_lines[1] = prefix..new_lines[1] +-- api.nvim_buf_set_lines(0, start.line, finish.line, false, new_lines) +end + +-- textDocument/completion response returns one of CompletionItem[], CompletionList or null. +-- https://microsoft.github.io/language-server-protocol/specification#textDocument_completion +function M.extract_completion_items(result) + if type(result) == 'table' and result.items then + return result.items + elseif result ~= nil then + return result + else + return {} + end +end + +--- Apply the TextDocumentEdit response. +-- @params TextDocumentEdit [table] see https://microsoft.github.io/language-server-protocol/specification +function M.text_document_apply_text_document_edit(text_document_edit, bufnr) + -- local text_document = text_document_edit.textDocument + -- TODO use text_document_version? + -- local text_document_version = text_document.version + + -- TODO technically, you could do this without doing multiple buf_get/set + -- by getting the full region (smallest line and largest line) and doing + -- the edits on the buffer, and then applying the buffer at the end. + -- I'm not sure if that's better. + for _, text_edit in ipairs(text_document_edit.edits) do + M.text_document_apply_text_edit(text_edit, bufnr) + end +end + +function M.get_current_line_to_cursor() + local pos = api.nvim_win_get_cursor(0) + local line = assert(api.nvim_buf_get_lines(0, pos[1]-1, pos[1], false)[1]) + return line:sub(pos[2]+1) +end + +--- Getting vim complete-items with incomplete flag. +-- @params CompletionItem[], CompletionList or nil (https://microsoft.github.io/language-server-protocol/specification#textDocument_completion) +-- @return { matches = complete-items table, incomplete = boolean } +function M.text_document_completion_list_to_complete_items(result, line_prefix) + local items = M.extract_completion_items(result) + if vim.tbl_isempty(items) then + return {} + end + -- Only initialize if we have some items. + if not line_prefix then + line_prefix = M.get_current_line_to_cursor() + end + + local matches = {} + + for _, completion_item in ipairs(items) do + local info = ' ' + local documentation = completion_item.documentation + if documentation then + if type(documentation) == 'string' and documentation ~= '' then + info = documentation + elseif type(documentation) == 'table' and type(documentation.value) == 'string' then + info = documentation.value + -- else + -- TODO(ashkan) Validation handling here? + end + end + + local word = completion_item.insertText or completion_item.label + + -- Ref: `:h complete-items` + table.insert(matches, { + word = remove_prefix(line_prefix, word), + abbr = completion_item.label, + kind = protocol.CompletionItemKind[completion_item.kind] or '', + menu = completion_item.detail or '', + info = info, + icase = 1, + dup = 0, + empty = 1, + }) + end + + return matches +end + +-- @params WorkspaceEdit [table] see https://microsoft.github.io/language-server-protocol/specification +function M.workspace_apply_workspace_edit(workspace_edit) + if workspace_edit.documentChanges then + for _, change in ipairs(workspace_edit.documentChanges) do + if change.kind then + -- TODO(ashkan) handle CreateFile/RenameFile/DeleteFile + error(string.format("Unsupported change: %q", vim.inspect(change))) + else + M.text_document_apply_text_document_edit(change) + end + end + return + end + + if workspace_edit.changes == nil or #workspace_edit.changes == 0 then + return + end + + for uri, changes in pairs(workspace_edit.changes) do + local fname = vim.uri_to_fname(uri) + -- TODO improve this approach. Try to edit open buffers without switching. + -- Not sure how to handle files which aren't open. This is deprecated + -- anyway, so I guess it could be left as is. + api.nvim_command('edit '..fname) + for _, change in ipairs(changes) do + M.text_document_apply_text_edit(change) + end + end +end + +--- Convert any of MarkedString | MarkedString[] | MarkupContent into markdown text lines +-- see https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#textDocument_hover +-- Useful for textDocument/hover, textDocument/signatureHelp, and potentially others. +function M.convert_input_to_markdown_lines(input, contents) + contents = contents or {} + -- MarkedString variation 1 + if type(input) == 'string' then + list_extend(contents, split_lines(input)) + else + assert(type(input) == 'table', "Expected a table for Hover.contents") + -- MarkupContent + if input.kind then + -- The kind can be either plaintext or markdown. However, either way we + -- will just be rendering markdown, so we handle them both the same way. + -- TODO these can have escaped/sanitized html codes in markdown. We + -- should make sure we handle this correctly. + + -- Some servers send input.value as empty, so let's ignore this :( + -- assert(type(input.value) == 'string') + list_extend(contents, split_lines(input.value or '')) + -- MarkupString variation 2 + elseif input.language then + -- Some servers send input.value as empty, so let's ignore this :( + -- assert(type(input.value) == 'string') + table.insert(contents, "```"..input.language) + list_extend(contents, split_lines(input.value or '')) + table.insert(contents, "```") + -- By deduction, this must be MarkedString[] + else + -- Use our existing logic to handle MarkedString + for _, marked_string in ipairs(input) do + M.convert_input_to_markdown_lines(marked_string, contents) + end + end + end + if contents[1] == '' or contents[1] == nil then + return {} + end + return contents +end + +function M.make_floating_popup_options(width, height, opts) + validate { + opts = { opts, 't', true }; + } + opts = opts or {} + validate { + ["opts.offset_x"] = { opts.offset_x, 'n', true }; + ["opts.offset_y"] = { opts.offset_y, 'n', true }; + } + + local anchor = '' + local row, col + + if vim.fn.winline() <= height then + anchor = anchor..'N' + row = 1 + else + anchor = anchor..'S' + row = 0 + end + + if vim.fn.wincol() + width <= api.nvim_get_option('columns') then + anchor = anchor..'W' + col = 0 + else + anchor = anchor..'E' + col = 1 + end + + return { + anchor = anchor, + col = col + (opts.offset_x or 0), + height = height, + relative = 'cursor', + row = row + (opts.offset_y or 0), + style = 'minimal', + width = width, + } +end + +function M.open_floating_preview(contents, filetype, opts) + validate { + contents = { contents, 't' }; + filetype = { filetype, 's', true }; + opts = { opts, 't', true }; + } + + -- Trim empty lines from the end. + for i = #contents, 1, -1 do + if #contents[i] == 0 then + table.remove(contents) + else + break + end + end + + local width = 0 + local height = #contents + for i, line in ipairs(contents) do + -- Clean up the input and add left pad. + line = " "..line:gsub("\r", "") + -- TODO(ashkan) use nvim_strdisplaywidth if/when that is introduced. + local line_width = vim.fn.strdisplaywidth(line) + width = math.max(line_width, width) + contents[i] = line + end + -- Add right padding of 1 each. + width = width + 1 + + local floating_bufnr = api.nvim_create_buf(false, true) + if filetype then + api.nvim_buf_set_option(floating_bufnr, 'filetype', filetype) + end + local float_option = M.make_floating_popup_options(width, height, opts) + local floating_winnr = api.nvim_open_win(floating_bufnr, false, float_option) + if filetype == 'markdown' then + api.nvim_win_set_option(floating_winnr, 'conceallevel', 2) + end + api.nvim_buf_set_lines(floating_bufnr, 0, -1, true, contents) + api.nvim_buf_set_option(floating_bufnr, 'modifiable', false) + api.nvim_command("autocmd CursorMoved ++once lua pcall(vim.api.nvim_win_close, "..floating_winnr..", true)") + return floating_bufnr, floating_winnr +end + +local function validate_lsp_position(pos) + validate { pos = {pos, 't'} } + validate { + line = {pos.line, 'n'}; + character = {pos.character, 'n'}; + } + return true +end + +function M.open_floating_peek_preview(bufnr, start, finish, opts) + validate { + bufnr = {bufnr, 'n'}; + start = {start, validate_lsp_position, 'valid start Position'}; + finish = {finish, validate_lsp_position, 'valid finish Position'}; + opts = { opts, 't', true }; + } + local width = math.max(finish.character - start.character + 1, 1) + local height = math.max(finish.line - start.line + 1, 1) + local floating_winnr = api.nvim_open_win(bufnr, false, M.make_floating_popup_options(width, height, opts)) + api.nvim_win_set_cursor(floating_winnr, {start.line+1, start.character}) + api.nvim_command("autocmd CursorMoved * ++once lua pcall(vim.api.nvim_win_close, "..floating_winnr..", true)") + return floating_winnr +end + + +local function highlight_range(bufnr, ns, hiname, start, finish) + if start[1] == finish[1] then + -- TODO care about encoding here since this is in byte index? + api.nvim_buf_add_highlight(bufnr, ns, hiname, start[1], start[2], finish[2]) + else + api.nvim_buf_add_highlight(bufnr, ns, hiname, start[1], start[2], -1) + for line = start[1] + 1, finish[1] - 1 do + api.nvim_buf_add_highlight(bufnr, ns, hiname, line, 0, -1) + end + api.nvim_buf_add_highlight(bufnr, ns, hiname, finish[1], 0, finish[2]) + end +end + +do + local all_buffer_diagnostics = {} + + local diagnostic_ns = api.nvim_create_namespace("vim_lsp_diagnostics") + + local default_severity_highlight = { + [protocol.DiagnosticSeverity.Error] = { guifg = "Red" }; + [protocol.DiagnosticSeverity.Warning] = { guifg = "Orange" }; + [protocol.DiagnosticSeverity.Information] = { guifg = "LightBlue" }; + [protocol.DiagnosticSeverity.Hint] = { guifg = "LightGrey" }; + } + + local underline_highlight_name = "LspDiagnosticsUnderline" + api.nvim_command(string.format("highlight %s gui=underline cterm=underline", underline_highlight_name)) + + local function find_color_rgb(color) + local rgb_hex = api.nvim_get_color_by_name(color) + validate { color = {color, function() return rgb_hex ~= -1 end, "valid color name"} } + return rgb_hex + end + + --- Determine whether to use black or white text + -- Ref: https://stackoverflow.com/a/1855903/837964 + -- https://stackoverflow.com/questions/596216/formula-to-determine-brightness-of-rgb-color + local function color_is_bright(r, g, b) + -- Counting the perceptive luminance - human eye favors green color + local luminance = (0.299*r + 0.587*g + 0.114*b)/255 + if luminance > 0.5 then + return true -- Bright colors, black font + else + return false -- Dark colors, white font + end + end + + local severity_highlights = {} + + function M.set_severity_highlights(highlights) + validate {highlights = {highlights, 't'}} + for severity, default_color in pairs(default_severity_highlight) do + local severity_name = protocol.DiagnosticSeverity[severity] + local highlight_name = "LspDiagnostics"..severity_name + local hi_info = highlights[severity] or default_color + -- Try to fill in the foreground color with a sane default. + if not hi_info.guifg and hi_info.guibg then + -- TODO(ashkan) move this out when bitop is guaranteed to be included. + local bit = require 'bit' + local band, rshift = bit.band, bit.rshift + local rgb = find_color_rgb(hi_info.guibg) + local is_bright = color_is_bright(rshift(rgb, 16), band(rshift(rgb, 8), 0xFF), band(rgb, 0xFF)) + hi_info.guifg = is_bright and "Black" or "White" + end + if not hi_info.ctermfg and hi_info.ctermbg then + -- TODO(ashkan) move this out when bitop is guaranteed to be included. + local bit = require 'bit' + local band, rshift = bit.band, bit.rshift + local rgb = find_color_rgb(hi_info.ctermbg) + local is_bright = color_is_bright(rshift(rgb, 16), band(rshift(rgb, 8), 0xFF), band(rgb, 0xFF)) + hi_info.ctermfg = is_bright and "Black" or "White" + end + local cmd_parts = {"highlight", highlight_name} + for k, v in pairs(hi_info) do + table.insert(cmd_parts, k.."="..v) + end + api.nvim_command(table.concat(cmd_parts, ' ')) + severity_highlights[severity] = highlight_name + end + end + + function M.buf_clear_diagnostics(bufnr) + validate { bufnr = {bufnr, 'n', true} } + bufnr = bufnr == 0 and api.nvim_get_current_buf() or bufnr + api.nvim_buf_clear_namespace(bufnr, diagnostic_ns, 0, -1) + end + + -- Initialize with the defaults. + M.set_severity_highlights(default_severity_highlight) + + function M.get_severity_highlight_name(severity) + return severity_highlights[severity] + end + + function M.show_line_diagnostics() + local bufnr = api.nvim_get_current_buf() + local line = api.nvim_win_get_cursor(0)[1] - 1 + -- local marks = api.nvim_buf_get_extmarks(bufnr, diagnostic_ns, {line, 0}, {line, -1}, {}) + -- if #marks == 0 then + -- return + -- end + -- local buffer_diagnostics = all_buffer_diagnostics[bufnr] + local lines = {"Diagnostics:"} + local highlights = {{0, "Bold"}} + + local buffer_diagnostics = all_buffer_diagnostics[bufnr] + if not buffer_diagnostics then return end + local line_diagnostics = buffer_diagnostics[line] + if not line_diagnostics then return end + + for i, diagnostic in ipairs(line_diagnostics) do + -- for i, mark in ipairs(marks) do + -- local mark_id = mark[1] + -- local diagnostic = buffer_diagnostics[mark_id] + + -- TODO(ashkan) make format configurable? + local prefix = string.format("%d. ", i) + local hiname = severity_highlights[diagnostic.severity] + local message_lines = split_lines(diagnostic.message) + table.insert(lines, prefix..message_lines[1]) + table.insert(highlights, {#prefix + 1, hiname}) + for j = 2, #message_lines do + table.insert(lines, message_lines[j]) + table.insert(highlights, {0, hiname}) + end + end + local popup_bufnr, winnr = M.open_floating_preview(lines, 'plaintext') + for i, hi in ipairs(highlights) do + local prefixlen, hiname = unpack(hi) + -- Start highlight after the prefix + api.nvim_buf_add_highlight(popup_bufnr, -1, hiname, i-1, prefixlen, -1) + end + return popup_bufnr, winnr + end + + function M.buf_diagnostics_save_positions(bufnr, diagnostics) + validate { + bufnr = {bufnr, 'n', true}; + diagnostics = {diagnostics, 't', true}; + } + if not diagnostics then return end + bufnr = bufnr == 0 and api.nvim_get_current_buf() or bufnr + + if not all_buffer_diagnostics[bufnr] then + -- Clean up our data when the buffer unloads. + api.nvim_buf_attach(bufnr, false, { + on_detach = function(b) + all_buffer_diagnostics[b] = nil + end + }) + end + all_buffer_diagnostics[bufnr] = {} + local buffer_diagnostics = all_buffer_diagnostics[bufnr] + + for _, diagnostic in ipairs(diagnostics) do + local start = diagnostic.range.start + -- local mark_id = api.nvim_buf_set_extmark(bufnr, diagnostic_ns, 0, start.line, 0, {}) + -- buffer_diagnostics[mark_id] = diagnostic + local line_diagnostics = buffer_diagnostics[start.line] + if not line_diagnostics then + line_diagnostics = {} + buffer_diagnostics[start.line] = line_diagnostics + end + table.insert(line_diagnostics, diagnostic) + end + end + + + function M.buf_diagnostics_underline(bufnr, diagnostics) + for _, diagnostic in ipairs(diagnostics) do + local start = diagnostic.range.start + local finish = diagnostic.range["end"] + + -- TODO care about encoding here since this is in byte index? + highlight_range(bufnr, diagnostic_ns, underline_highlight_name, + {start.line, start.character}, + {finish.line, finish.character} + ) + end + end + + function M.buf_diagnostics_virtual_text(bufnr, diagnostics) + local buffer_line_diagnostics = all_buffer_diagnostics[bufnr] + if not buffer_line_diagnostics then + M.buf_diagnostics_save_positions(bufnr, diagnostics) + end + buffer_line_diagnostics = all_buffer_diagnostics[bufnr] + if not buffer_line_diagnostics then + return + end + for line, line_diags in pairs(buffer_line_diagnostics) do + local virt_texts = {} + for i = 1, #line_diags - 1 do + table.insert(virt_texts, {"■", severity_highlights[line_diags[i].severity]}) + end + local last = line_diags[#line_diags] + -- TODO(ashkan) use first line instead of subbing 2 spaces? + table.insert(virt_texts, {"■ "..last.message:gsub("\r", ""):gsub("\n", " "), severity_highlights[last.severity]}) + api.nvim_buf_set_virtual_text(bufnr, diagnostic_ns, line, virt_texts, {}) + end + end +end + +function M.buf_loclist(bufnr, locations) + local targetwin + for _, winnr in ipairs(api.nvim_list_wins()) do + local winbuf = api.nvim_win_get_buf(winnr) + if winbuf == bufnr then + targetwin = winnr + break + end + end + if not targetwin then return end + + local items = {} + local path = api.nvim_buf_get_name(bufnr) + for _, d in ipairs(locations) do + -- TODO: URL parsing here? + local start = d.range.start + table.insert(items, { + filename = path, + lnum = start.line + 1, + col = start.character + 1, + text = d.message, + }) + end + vim.fn.setloclist(targetwin, items, ' ', 'Language Server') +end + +return M +-- vim:sw=2 ts=2 et diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index e987e07a2a..ff89acc524 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -98,6 +98,38 @@ function vim.split(s,sep,plain) return t end +--- Return a list of all keys used in a table. +--- However, the order of the return table of keys is not guaranteed. +--- +--@see From https://github.com/premake/premake-core/blob/master/src/base/table.lua +--- +--@param t Table +--@returns list of keys +function vim.tbl_keys(t) + assert(type(t) == 'table', string.format("Expected table, got %s", type(t))) + + local keys = {} + for k, _ in pairs(t) do + table.insert(keys, k) + end + return keys +end + +--- Return a list of all values used in a table. +--- However, the order of the return table of values is not guaranteed. +--- +--@param t Table +--@returns list of values +function vim.tbl_values(t) + assert(type(t) == 'table', string.format("Expected table, got %s", type(t))) + + local values = {} + for _, v in pairs(t) do + table.insert(values, v) + end + return values +end + --- Checks if a list-like (vector) table contains `value`. --- --@param t Table to check @@ -114,6 +146,16 @@ function vim.tbl_contains(t, value) return false end +-- Returns true if the table is empty, and contains no indexed or keyed values. +-- +--@see From https://github.com/premake/premake-core/blob/master/src/base/table.lua +-- +--@param t Table to check +function vim.tbl_isempty(t) + assert(type(t) == 'table', string.format("Expected table, got %s", type(t))) + return next(t) == nil +end + --- Merges two or more map-like tables. --- --@see |extend()| @@ -145,13 +187,69 @@ function vim.tbl_extend(behavior, ...) return ret end +--- Deep compare values for equality +function vim.deep_equal(a, b) + if a == b then return true end + if type(a) ~= type(b) then return false end + if type(a) == 'table' then + -- TODO improve this algorithm's performance. + for k, v in pairs(a) do + if not vim.deep_equal(v, b[k]) then + return false + end + end + for k, v in pairs(b) do + if not vim.deep_equal(v, a[k]) then + return false + end + end + return true + end + return false +end + +--- Add the reverse lookup values to an existing table. +--- For example: +--- `tbl_add_reverse_lookup { A = 1 } == { [1] = 'A', A = 1 }` +-- +--Do note that it *modifies* the input. +--@param o table The table to add the reverse to. +function vim.tbl_add_reverse_lookup(o) + local keys = vim.tbl_keys(o) + for _, k in ipairs(keys) do + local v = o[k] + if o[v] then + error(string.format("The reverse lookup found an existing value for %q while processing key %q", tostring(v), tostring(k))) + end + o[v] = k + end + return o +end + +--- Extends a list-like table with the values of another list-like table. +--- +--NOTE: This *mutates* dst! +--@see |extend()| +--- +--@param dst The list which will be modified and appended to. +--@param src The list from which values will be inserted. +function vim.list_extend(dst, src) + assert(type(dst) == 'table', "dst must be a table") + assert(type(src) == 'table', "src must be a table") + for _, v in ipairs(src) do + table.insert(dst, v) + end + return dst +end + --- Creates a copy of a list-like table such that any nested tables are --- "unrolled" and appended to the result. --- +--@see From https://github.com/premake/premake-core/blob/master/src/base/table.lua +--- --@param t List-like table --@returns Flattened copy of the given list-like table. function vim.tbl_flatten(t) - -- From https://github.com/premake/premake-core/blob/master/src/base/table.lua local result = {} local function _tbl_flatten(_t) local n = #_t @@ -168,6 +266,32 @@ function vim.tbl_flatten(t) return result end +-- Determine whether a Lua table can be treated as an array. +--- +--@params Table +--@returns true: A non-empty array, false: A non-empty table, nil: An empty table +function vim.tbl_islist(t) + if type(t) ~= 'table' then + return false + end + + local count = 0 + + for k, _ in pairs(t) do + if type(k) == "number" then + count = count + 1 + else + return false + end + end + + if count > 0 then + return true + else + return nil + end +end + --- Trim whitespace (Lua pattern "%s") from both sides of a string. --- --@see https://www.lua.org/pil/20.2.html @@ -279,3 +403,4 @@ function vim.is_callable(f) end return vim +-- vim:sw=2 ts=2 et diff --git a/runtime/lua/vim/uri.lua b/runtime/lua/vim/uri.lua new file mode 100644 index 0000000000..0a6e0fcb97 --- /dev/null +++ b/runtime/lua/vim/uri.lua @@ -0,0 +1,89 @@ +--- TODO: This is implemented only for files now. +-- https://tools.ietf.org/html/rfc3986 +-- https://tools.ietf.org/html/rfc2732 +-- https://tools.ietf.org/html/rfc2396 + + +local uri_decode +do + local schar = string.char + local function hex_to_char(hex) + return schar(tonumber(hex, 16)) + end + uri_decode = function(str) + return str:gsub("%%([a-fA-F0-9][a-fA-F0-9])", hex_to_char) + end +end + +local uri_encode +do + local PATTERNS = { + --- RFC 2396 + -- https://tools.ietf.org/html/rfc2396#section-2.2 + rfc2396 = "^A-Za-z0-9%-_.!~*'()"; + --- RFC 2732 + -- https://tools.ietf.org/html/rfc2732 + rfc2732 = "^A-Za-z0-9%-_.!~*'()[]"; + --- RFC 3986 + -- https://tools.ietf.org/html/rfc3986#section-2.2 + rfc3986 = "^A-Za-z0-9%-._~!$&'()*+,;=:@/"; + } + local sbyte, tohex = string.byte + if jit then + tohex = require'bit'.tohex + else + tohex = function(b) return string.format("%02x", b) end + end + local function percent_encode_char(char) + return "%"..tohex(sbyte(char), 2) + end + uri_encode = function(text, rfc) + if not text then return end + local pattern = PATTERNS[rfc] or PATTERNS.rfc3986 + return text:gsub("(["..pattern.."])", percent_encode_char) + end +end + + +local function is_windows_file_uri(uri) + return uri:match('^file:///[a-zA-Z]:') ~= nil +end + +local function uri_from_fname(path) + local volume_path, fname = path:match("^([a-zA-Z]:)(.*)") + local is_windows = volume_path ~= nil + if is_windows then + path = volume_path..uri_encode(fname:gsub("\\", "/")) + else + path = uri_encode(path) + end + local uri_parts = {"file://"} + if is_windows then + table.insert(uri_parts, "/") + end + table.insert(uri_parts, path) + return table.concat(uri_parts) +end + +local function uri_from_bufnr(bufnr) + return uri_from_fname(vim.api.nvim_buf_get_name(bufnr)) +end + +local function uri_to_fname(uri) + -- TODO improve this. + if is_windows_file_uri(uri) then + uri = uri:gsub('^file:///', '') + uri = uri:gsub('/', '\\') + else + uri = uri:gsub('^file://', '') + end + + return uri_decode(uri) +end + +return { + uri_from_fname = uri_from_fname, + uri_from_bufnr = uri_from_bufnr, + uri_to_fname = uri_to_fname, +} +-- vim:sw=2 ts=2 et -- cgit From 69a0712a9c2bae0969f7a84e5ec321ce6120982b Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 20 Nov 2019 11:23:50 -0800 Subject: Reduce code blocks in markdown previews. If the preview is just a code block, then use the language as the filetype instead of using markdown. This reduces the size of the preview. --- runtime/lua/vim/lsp/builtin_callbacks.lua | 54 ++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/builtin_callbacks.lua b/runtime/lua/vim/lsp/builtin_callbacks.lua index cc739ce3ad..717fda395e 100644 --- a/runtime/lua/vim/lsp/builtin_callbacks.lua +++ b/runtime/lua/vim/lsp/builtin_callbacks.lua @@ -81,6 +81,56 @@ builtin_callbacks['textDocument/publishDiagnostics'] = function(_, _, result) -- util.buf_loclist(bufnr, result.diagnostics) end +local function focusable_popup() + local popup_win + return function(winnr) + if popup_win and nvim.win_is_valid(popup_win) then + if nvim.get_current_win() == popup_win then + nvim.ex.wincmd "p" + else + nvim.set_current_win(popup_win) + end + return + end + popup_win = winnr + end +end + +local hover_popup = focusable_popup() + +local function trim_empty_lines(lines) + local result = {} + for _, line in ipairs(lines) do + if #line > 0 then + table.insert(result, line) + end + end + return result +end + +-- Accepts markdown lines and tries to reduce it to a filetype if it is +-- just a single code block. +local function try_trim_code_blocks(lines) + local language_id = lines[1]:match("^```(.*)") + if language_id then + local has_inner_code_fence = false + for i = 2, (#lines - 1) do + local line = lines[i] + if line:sub(1,3) == '```' then + has_inner_code_fence = true + break + end + end + -- No inner code fences + starting with code fence = hooray. + if not has_inner_code_fence then + table.remove(lines, 1) + table.remove(lines) + return lines, language_id + end + end + return lines, 'markdown' +end + -- textDocument/hover -- https://microsoft.github.io/language-server-protocol/specification#textDocument_hover -- @params MarkedString | MarkedString[] | MarkupContent @@ -91,10 +141,12 @@ builtin_callbacks['textDocument/hover'] = function(_, _, result) if result.contents ~= nil then local markdown_lines = util.convert_input_to_markdown_lines(result.contents) + markdown_lines = trim_empty_lines(markdown_lines) if vim.tbl_isempty(markdown_lines) then markdown_lines = { 'No information available' } end - util.open_floating_preview(markdown_lines, 'markdown') + local _, winnr = util.open_floating_preview(try_trim_code_blocks(markdown_lines)) + hover_popup(winnr) end end -- cgit From 3ae9b3781e0b66f5874971665b38314d61668627 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 20 Nov 2019 11:34:10 -0800 Subject: Bugfix for floating_preview Don't modify your inputs. --- runtime/lua/vim/lsp/util.lua | 3 +++ 1 file changed, 3 insertions(+) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index f96e0f01a8..707e2d0d3b 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -262,6 +262,9 @@ function M.open_floating_preview(contents, filetype, opts) opts = { opts, 't', true }; } + -- Don't modify our inputs. + contents = vim.deepcopy(contents) + -- Trim empty lines from the end. for i = #contents, 1, -1 do if #contents[i] == 0 then -- cgit From 0904ffe387f825404d8467f9787678c2251728dd Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 20 Nov 2019 11:36:50 -0800 Subject: Bugfixes. - Return after an error in RPC. - Use an empty vim table for serialization. --- runtime/lua/vim/lsp.lua | 2 +- runtime/lua/vim/lsp/rpc.lua | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 9dbe03dace..4724ff0281 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -400,7 +400,7 @@ function lsp.start_client(config) rpc.request('initialize', initialize_params, function(init_err, result) assert(not init_err, tostring(init_err)) assert(result, "server sent empty result") - rpc.notify('initialized', {}) + rpc.notify('initialized', {[vim.type_idx]=vim.types.dictionary}) client.initialized = true uninitialized_clients[client_id] = nil client.server_capabilities = assert(result.capabilities, "initialize result doesn't contain capabilities") diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua index e0ec8863d6..a558f66a42 100644 --- a/runtime/lua/vim/lsp/rpc.lua +++ b/runtime/lua/vim/lsp/rpc.lua @@ -340,6 +340,7 @@ local function create_and_start_client(cmd, cmd_args, handlers, extra_spawn_para local decoded, err = json_decode(body) if not decoded then on_error(client_errors.INVALID_SERVER_JSON, err) + return end local _ = log.debug() and log.debug("decoded", decoded) -- cgit From 568b4540884bb852b733dfcdd6a2da0d4fe2b42e Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 20 Nov 2019 11:39:54 -0800 Subject: Add vim.uri_to_bufnr --- runtime/lua/vim/lsp/builtin_callbacks.lua | 8 ++------ runtime/lua/vim/uri.lua | 7 ++++++- 2 files changed, 8 insertions(+), 7 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/builtin_callbacks.lua b/runtime/lua/vim/lsp/builtin_callbacks.lua index 717fda395e..a5e1162d02 100644 --- a/runtime/lua/vim/lsp/builtin_callbacks.lua +++ b/runtime/lua/vim/lsp/builtin_callbacks.lua @@ -62,14 +62,10 @@ builtin_callbacks['textDocument/rename'] = function(_, _, result) util.workspace_apply_workspace_edit(result) end -local function uri_to_bufnr(uri) - return vim.fn.bufadd((vim.uri_to_fname(uri))) -end - builtin_callbacks['textDocument/publishDiagnostics'] = function(_, _, result) if not result then return end local uri = result.uri - local bufnr = uri_to_bufnr(uri) + local bufnr = vim.uri_to_bufnr(uri) if not bufnr then api.nvim_err_writeln(string.format("LSP.publishDiagnostics: Couldn't find buffer for %s", uri)) return @@ -154,7 +150,7 @@ builtin_callbacks['textDocument/peekDefinition'] = function(_, _, result) if result == nil or vim.tbl_isempty(result) then return end -- TODO(ashkan) what to do with multiple locations? result = result[1] - local bufnr = uri_to_bufnr(result.uri) + local bufnr = vim.uri_to_bufnr(result.uri) assert(bufnr) local start = result.range.start local finish = result.range["end"] diff --git a/runtime/lua/vim/uri.lua b/runtime/lua/vim/uri.lua index 0a6e0fcb97..1065f84f4c 100644 --- a/runtime/lua/vim/uri.lua +++ b/runtime/lua/vim/uri.lua @@ -77,13 +77,18 @@ local function uri_to_fname(uri) else uri = uri:gsub('^file://', '') end - return uri_decode(uri) end +-- Return or create a buffer for a uri. +local function uri_to_bufnr(uri) + return vim.fn.bufadd((uri_to_fname(uri))) +end + return { uri_from_fname = uri_from_fname, uri_from_bufnr = uri_from_bufnr, uri_to_fname = uri_to_fname, + uri_to_bufnr = uri_to_bufnr, } -- vim:sw=2 ts=2 et -- cgit From ff65cc990afba288df6d39d6f95901cf0786c668 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 20 Nov 2019 11:41:45 -0800 Subject: Bugfix. Don't use nvim.lua that doesn't exist :) --- runtime/lua/vim/lsp/builtin_callbacks.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/builtin_callbacks.lua b/runtime/lua/vim/lsp/builtin_callbacks.lua index a5e1162d02..2229547f20 100644 --- a/runtime/lua/vim/lsp/builtin_callbacks.lua +++ b/runtime/lua/vim/lsp/builtin_callbacks.lua @@ -80,11 +80,11 @@ end local function focusable_popup() local popup_win return function(winnr) - if popup_win and nvim.win_is_valid(popup_win) then - if nvim.get_current_win() == popup_win then - nvim.ex.wincmd "p" + if popup_win and api.nvim_win_is_valid(popup_win) then + if api.nvim_get_current_win() == popup_win then + api.nvim_command("wincmd p") else - nvim.set_current_win(popup_win) + api.nvim_set_current_win(popup_win) end return end -- cgit From c83380cf80561d45c067db5e70c7275eda7671af Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 20 Nov 2019 14:21:57 -0800 Subject: Add lsp.buf and hover implementation. --- runtime/lua/vim/lsp/buf.lua | 85 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 runtime/lua/vim/lsp/buf.lua (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua new file mode 100644 index 0000000000..6398bcca74 --- /dev/null +++ b/runtime/lua/vim/lsp/buf.lua @@ -0,0 +1,85 @@ +local validate = vim.validate +local api = vim.api +local util = require 'vim.lsp.util' +local protocol = require 'vim.lsp.protocol' + +local M = {} + +local function resolve_bufnr(bufnr) + validate { bufnr = { bufnr, 'n', true } } + if bufnr == nil or bufnr == 0 then + return api.nvim_get_current_buf() + end + return bufnr +end + +local function ok_or_nil(status, ...) + if not status then return end + return ... +end +local function npcall(fn, ...) + return ok_or_nil(pcall(fn, ...)) +end + +local function find_window_by_var(name, value) + for _, win in ipairs(api.nvim_list_wins()) do + if npcall(api.nvim_win_get_var, win, name) == value then + return win + end + end +end + +local hover_window_tag = 'lsp_hover' +function M.hover(bufnr) + if npcall(api.nvim_win_get_var, 0, hover_window_tag) then + api.nvim_command("wincmd p") + return + end + + bufnr = resolve_bufnr(bufnr) + do + local win = find_window_by_var(hover_window_tag, bufnr) + if win then + api.nvim_set_current_win(win) + return + end + end + local params = protocol.make_text_document_position_params() + vim.lsp.buf_request(bufnr, 'textDocument/hover', params, function(_, _, result, _) + if result == nil or vim.tbl_isempty(result) then + return + end + + if result.contents ~= nil then + local markdown_lines = util.convert_input_to_markdown_lines(result.contents) + markdown_lines = util.trim_empty_lines(markdown_lines) + if vim.tbl_isempty(markdown_lines) then + markdown_lines = { 'No information available' } + end + local filetype = util.try_trim_markdown_code_blocks(markdown_lines) + local _, winnr = util.open_floating_preview(markdown_lines, filetype) + api.nvim_win_set_var(winnr, hover_window_tag, bufnr) + end + end) +end + +function M.signature_help() +end + +function M.declaration() +end + +function M.type_definition() +end + +function M.implementation() +end + +-- TODO(ashkan) ? +function M.completion() +end + +function M.range_formatting() +end + +return M -- cgit From 2d580756ca707945c01703d404e10f4bf412a72c Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 20 Nov 2019 15:35:18 -0800 Subject: Add everything to lsp.buf and get rid of autoload. --- runtime/lua/vim/lsp/buf.lua | 161 ++++++++++++++++++++++++++++++++++++------- runtime/lua/vim/lsp/util.lua | 88 +++++++++++++++++------ 2 files changed, 204 insertions(+), 45 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 6398bcca74..5f54196dcc 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -1,7 +1,9 @@ local validate = vim.validate local api = vim.api +local vfn = vim.fn local util = require 'vim.lsp.util' local protocol = require 'vim.lsp.protocol' +local log = require 'vim.lsp.log' local M = {} @@ -29,54 +31,165 @@ local function find_window_by_var(name, value) end end -local hover_window_tag = 'lsp_hover' -function M.hover(bufnr) - if npcall(api.nvim_win_get_var, 0, hover_window_tag) then - api.nvim_command("wincmd p") - return +local function request(method, params, callback) + -- TODO(ashkan) enable this. + -- callback = vim.lsp.default_callback[method] or callback + validate { + method = {method, 's'}; + callback = {callback, 'f'}; + } + return vim.lsp.buf_request(0, method, params, function(err, _, result, client_id) + if err then error(tostring(err)) end + return callback(err, method, result, client_id) + end) +end + +local function focusable_preview(method, params, fn) + if npcall(api.nvim_win_get_var, 0, method) then + return api.nvim_command("wincmd p") end - bufnr = resolve_bufnr(bufnr) + local bufnr = api.nvim_get_current_buf() do - local win = find_window_by_var(hover_window_tag, bufnr) + local win = find_window_by_var(method, bufnr) if win then - api.nvim_set_current_win(win) - return + return api.nvim_set_current_win(win) end end - local params = protocol.make_text_document_position_params() - vim.lsp.buf_request(bufnr, 'textDocument/hover', params, function(_, _, result, _) - if result == nil or vim.tbl_isempty(result) then - return + return request(method, params, function(_, _, result, _) + -- TODO(ashkan) could show error in preview... + local lines, filetype, opts = fn(result) + if lines then + local _, winnr = util.open_floating_preview(lines, filetype, opts) + api.nvim_win_set_var(winnr, method, bufnr) end + end) +end + +function M.hover() + local params = protocol.make_text_document_position_params() + focusable_preview('textDocument/hover', params, function(result) + if not (result and result.contents) then return end - if result.contents ~= nil then - local markdown_lines = util.convert_input_to_markdown_lines(result.contents) - markdown_lines = util.trim_empty_lines(markdown_lines) - if vim.tbl_isempty(markdown_lines) then - markdown_lines = { 'No information available' } - end - local filetype = util.try_trim_markdown_code_blocks(markdown_lines) - local _, winnr = util.open_floating_preview(markdown_lines, filetype) - api.nvim_win_set_var(winnr, hover_window_tag, bufnr) + local markdown_lines = util.convert_input_to_markdown_lines(result.contents) + markdown_lines = util.trim_empty_lines(markdown_lines) + if vim.tbl_isempty(markdown_lines) then + return { 'No information available' } end + return markdown_lines, util.try_trim_markdown_code_blocks(markdown_lines) end) end -function M.signature_help() +function M.peek_definition() + local params = protocol.make_text_document_position_params() + request('textDocument/peekDefinition', params, function(_, _, result, _) + if not (result and result[1]) then return end + local loc = result[1] + local bufnr = vim.uri_to_bufnr(loc.uri) or error("couldn't find file "..tostring(loc.uri)) + local start = loc.range.start + local finish = loc.range["end"] + util.open_floating_peek_preview(bufnr, start, finish, { offset_x = 1 }) + local headbuf = util.open_floating_preview({"Peek:"}, nil, { + offset_y = -(finish.line - start.line); + width = finish.character - start.character + 2; + }) + -- TODO(ashkan) change highlight group? + api.nvim_buf_add_highlight(headbuf, -1, 'Keyword', 0, -1) + end) +end + + +local function update_tagstack() + local bufnr = api.nvim_get_current_buf() + local line = vfn.line('.') + local col = vfn.col('.') + local tagname = vfn.expand('') + local item = { bufnr = bufnr, from = { bufnr, line, col, 0 }, tagname = tagname } + local winid = vfn.win_getid() + local tagstack = vfn.gettagstack(winid) + local action + if tagstack.length == tagstack.curidx then + action = 'r' + tagstack.items[tagstack.curidx] = item + elseif tagstack.length > tagstack.curidx then + action = 'r' + if tagstack.curidx > 1 then + tagstack.items = table.insert(tagstack.items[tagstack.curidx - 1], item) + else + tagstack.items = { item } + end + else + action = 'a' + tagstack.items = { item } + end + tagstack.curidx = tagstack.curidx + 1 + vfn.settagstack(winid, tagstack, action) +end +local function handle_location(result) + -- We can sometimes get a list of locations, so set the first value as the + -- only value we want to handle + -- TODO(ashkan) was this correct^? We could use location lists. + if result[1] ~= nil then + result = result[1] + end + if result.uri == nil then + api.nvim_err_writeln('[LSP] Could not find a valid location') + return + end + local result_file = vim.uri_to_fname(result.uri) + local bufnr = vfn.bufadd(result_file) + update_tagstack() + api.nvim_set_current_buf(bufnr) + local start = result.range.start + api.nvim_win_set_cursor(0, {start.line + 1, start.character}) + return true +end +local function location_callback(_, method, result) + if result == nil or vim.tbl_isempty(result) then + local _ = log.info() and log.info(method, 'No location found') + return nil + end + return handle_location(result) end function M.declaration() + local params = protocol.make_text_document_position_params() + request('textDocument/declaration', params, location_callback) +end + +function M.definition() + local params = protocol.make_text_document_position_params() + request('textDocument/definition', params, location_callback) end function M.type_definition() + local params = protocol.make_text_document_position_params() + request('textDocument/typeDefinition', params, location_callback) end function M.implementation() + local params = protocol.make_text_document_position_params() + request('textDocument/implementation', params, location_callback) +end + +function M.signature_help() + local params = protocol.make_text_document_position_params() + request('textDocument/signatureHelp', params, location_callback) end -- TODO(ashkan) ? -function M.completion() +function M.completion(context) + local params = protocol.make_text_document_position_params() + params.context = context + return request('textDocument/completion', params, function(_, _, result) + if vim.tbl_isempty(result or {}) then return end + local row, col = unpack(api.nvim_win_get_cursor(0)) + local line = assert(api.nvim_buf_get_lines(0, row-1, row, false)[1]) + local line_to_cursor = line:sub(col+1) + + local matches = util.text_document_completion_list_to_complete_items(result, line_to_cursor) + vim.fn.complete(col, matches) + end) end function M.range_formatting() diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 707e2d0d3b..13c83fefd6 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -261,32 +261,27 @@ function M.open_floating_preview(contents, filetype, opts) filetype = { filetype, 's', true }; opts = { opts, 't', true }; } - - -- Don't modify our inputs. - contents = vim.deepcopy(contents) + opts = opts or {} -- Trim empty lines from the end. - for i = #contents, 1, -1 do - if #contents[i] == 0 then - table.remove(contents) - else - break + contents = M.trim_empty_lines(contents) + + local width = opts.width + local height = opts.height or #contents + if not width then + width = 0 + for i, line in ipairs(contents) do + -- Clean up the input and add left pad. + line = " "..line:gsub("\r", "") + -- TODO(ashkan) use nvim_strdisplaywidth if/when that is introduced. + local line_width = vim.fn.strdisplaywidth(line) + width = math.max(line_width, width) + contents[i] = line end + -- Add right padding of 1 each. + width = width + 1 end - local width = 0 - local height = #contents - for i, line in ipairs(contents) do - -- Clean up the input and add left pad. - line = " "..line:gsub("\r", "") - -- TODO(ashkan) use nvim_strdisplaywidth if/when that is introduced. - local line_width = vim.fn.strdisplaywidth(line) - width = math.max(line_width, width) - contents[i] = line - end - -- Add right padding of 1 each. - width = width + 1 - local floating_bufnr = api.nvim_create_buf(false, true) if filetype then api.nvim_buf_set_option(floating_bufnr, 'filetype', filetype) @@ -556,5 +551,56 @@ function M.buf_loclist(bufnr, locations) vim.fn.setloclist(targetwin, items, ' ', 'Language Server') end +-- Remove empty lines from the beginning and end. +function M.trim_empty_lines(lines) + local result = {} + local start = 1 + for i = 1, #lines do + if #lines[i] > 0 then + start = i + break + end + end + local finish = 1 + for i = #lines, 1, -1 do + if #lines[i] > 0 then + finish = i + break + end + end + -- TODO(ashkan) use tbl_slice. + for i = start, finish do + table.insert(result, lines[i]) + end + return result +end + +-- Accepts markdown lines and tries to reduce it to a filetype if it is +-- just a single code block. +-- Note: This modifies the input. +-- +-- Returns: filetype or 'markdown' if it was unchanged. +function M.try_trim_markdown_code_blocks(lines) + local language_id = lines[1]:match("^```(.*)") + if language_id then + local has_inner_code_fence = false + for i = 2, (#lines - 1) do + local line = lines[i] + if line:sub(1,3) == '```' then + has_inner_code_fence = true + break + end + end + -- No inner code fences + starting with code fence = hooray. + if not has_inner_code_fence then + table.remove(lines, 1) + table.remove(lines) + return language_id + end + end + return 'markdown' +end + + return M -- vim:sw=2 ts=2 et -- cgit From a4b7004f489030d9ee7e3bbfc156ab540744279b Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 20 Nov 2019 16:03:32 -0800 Subject: Move everything to buf & default_callbacks - Rename builtin_callbacks to default_callbacks and slightly change its semantics: - No longer contains the default implementations. Instead, any default_callbacks will be used in preference for our .buf methods. - Add this to the docs. --- runtime/lua/vim/lsp.lua | 137 +----------- runtime/lua/vim/lsp/buf.lua | 86 +++++++- runtime/lua/vim/lsp/builtin_callbacks.lua | 344 ------------------------------ runtime/lua/vim/lsp/default_callbacks.lua | 57 +++++ runtime/lua/vim/lsp/protocol.lua | 1 - 5 files changed, 147 insertions(+), 478 deletions(-) delete mode 100644 runtime/lua/vim/lsp/builtin_callbacks.lua create mode 100644 runtime/lua/vim/lsp/default_callbacks.lua (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 4724ff0281..44909a85ee 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -1,4 +1,4 @@ -local builtin_callbacks = require 'vim.lsp.builtin_callbacks' +local default_callbacks = require 'vim.lsp.default_callbacks' local log = require 'vim.lsp.log' local lsp_rpc = require 'vim.lsp.rpc' local protocol = require 'vim.lsp.protocol' @@ -12,7 +12,8 @@ local validate = vim.validate local lsp = { protocol = protocol; - builtin_callbacks = builtin_callbacks; + default_callbacks = default_callbacks; + buf = require'vim.lsp.buf'; util = util; -- Allow raw RPC access. rpc = lsp_rpc; @@ -283,7 +284,7 @@ function lsp.start_client(config) local client_id = next_client_id() - local callbacks = tbl_extend("keep", config.callbacks or {}, builtin_callbacks) + local callbacks = tbl_extend("keep", config.callbacks or {}, default_callbacks) -- Copy metatable if it has one. if config.callbacks and config.callbacks.__metatable then setmetatable(callbacks, getmetatable(config.callbacks)) @@ -869,134 +870,8 @@ function lsp.omnifunc(findstart, base) end end ---- ---- FileType based configuration utility ---- - -local all_filetype_configs = {} - --- Lookup a filetype config client by its name. -function lsp.get_filetype_client_by_name(name) - local config = all_filetype_configs[name] - if config.client_id then - return active_clients[config.client_id] - end -end - -local function start_filetype_config(config) - config.client_id = lsp.start_client(config) - nvim_command(string.format( - "autocmd FileType %s silent lua vim.lsp.buf_attach_client(0, %d)", - table.concat(config.filetypes, ','), - config.client_id)) - return config.client_id -end - --- Easy configuration option for common LSP use-cases. --- This will lazy initialize the client when the filetypes specified are --- encountered and attach to those buffers. --- --- The configuration options are the same as |vim.lsp.start_client()|, but --- with a few additions and distinctions: --- --- Additional parameters: --- - filetype: {string} or {list} of filetypes to attach to. --- - name: A unique string among all other servers configured with --- |vim.lsp.add_filetype_config|. --- --- Differences: --- - root_dir: will default to |getcwd()| --- -function lsp.add_filetype_config(config) - -- Additional defaults. - -- Keep a copy of the user's input for debugging reasons. - local user_config = config - config = tbl_extend("force", {}, user_config) - config.root_dir = config.root_dir or uv.cwd() - -- Validate config. - validate_client_config(config) - validate { - name = { config.name, 's' }; - } - assert(config.filetype, "config must have 'filetype' key") - - local filetypes - if type(config.filetype) == 'string' then - filetypes = { config.filetype } - elseif type(config.filetype) == 'table' then - filetypes = config.filetype - assert(not tbl_isempty(filetypes), "config.filetype must not be an empty table") - else - error("config.filetype must be a string or a list of strings") - end - - if all_filetype_configs[config.name] then - -- If the client exists, then it is likely that they are doing some kind of - -- reload flow, so let's not throw an error here. - if all_filetype_configs[config.name].client_id then - -- TODO log here? It might be unnecessarily annoying. - return - end - error(string.format('A configuration with the name %q already exists. They must be unique', config.name)) - end - - all_filetype_configs[config.name] = tbl_extend("keep", config, { - client_id = nil; - filetypes = filetypes; - user_config = user_config; - }) - - nvim_command(string.format( - "autocmd FileType %s ++once silent lua vim.lsp._start_filetype_config_client(%q)", - table.concat(filetypes, ','), - config.name)) -end - --- Create a copy of an existing configuration, and override config with values --- from new_config. --- This is useful if you wish you create multiple LSPs with different root_dirs --- or other use cases. --- --- You can specify a new unique name, but if you do not, a unique name will be --- created like `name-dup_count`. --- --- existing_name: the name of the existing config to copy. --- new_config: the new configuration options. @see |vim.lsp.start_client()|. --- @returns string the new name. -function lsp.copy_filetype_config(existing_name, new_config) - local config = all_filetype_configs[existing_name] - or error(string.format("Configuration with name %q doesn't exist", existing_name)) - config = tbl_extend("force", config, new_config or {}) - config.client_id = nil - config.original_config_name = existing_name - - -- If the user didn't rename it, we will. - if config.name == existing_name then - -- Create a new, unique name. - local duplicate_count = 0 - for _, conf in pairs(all_filetype_configs) do - if conf.original_config_name == existing_name then - duplicate_count = duplicate_count + 1 - end - end - config.name = string.format("%s-%d", existing_name, duplicate_count + 1) - end - print("New config name:", config.name) - lsp.add_filetype_config(config) - return config.name -end - --- Autocmd handler to actually start the client when an applicable filetype is --- encountered. -function lsp._start_filetype_config_client(name) - local config = all_filetype_configs[name] - -- If it exists and is running, don't make it again. - if config.client_id and active_clients[config.client_id] then - -- TODO log here? - return - end - lsp.buf_attach_client(0, start_filetype_config(config)) - return config.client_id +function lsp.client_is_stopped(client_id) + return active_clients[client_id] == nil end --- diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 5f54196dcc..e8a38aa6ef 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -33,7 +33,7 @@ end local function request(method, params, callback) -- TODO(ashkan) enable this. - -- callback = vim.lsp.default_callback[method] or callback + -- callback = vim.lsp.default_callbacks[method] or callback validate { method = {method, 's'}; callback = {callback, 'f'}; @@ -172,9 +172,78 @@ function M.implementation() request('textDocument/implementation', params, location_callback) end +--- Convert SignatureHelp response to preview contents. +-- https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#textDocument_signatureHelp +local function signature_help_to_preview_contents(input) + if not input.signatures then + return + end + --The active signature. If omitted or the value lies outside the range of + --`signatures` the value defaults to zero or is ignored if `signatures.length + --=== 0`. Whenever possible implementors should make an active decision about + --the active signature and shouldn't rely on a default value. + local contents = {} + local active_signature = input.activeSignature or 0 + -- If the activeSignature is not inside the valid range, then clip it. + if active_signature >= #input.signatures then + active_signature = 0 + end + local signature = input.signatures[active_signature + 1] + if not signature then + return + end + vim.list_extend(contents, split_lines(signature.label)) + if signature.documentation then + util.convert_input_to_markdown_lines(signature.documentation, contents) + end + if input.parameters then + local active_parameter = input.activeParameter or 0 + -- If the activeParameter is not inside the valid range, then clip it. + if active_parameter >= #input.parameters then + active_parameter = 0 + end + local parameter = signature.parameters and signature.parameters[active_parameter] + if parameter then + --[=[ + --Represents a parameter of a callable-signature. A parameter can + --have a label and a doc-comment. + interface ParameterInformation { + --The label of this parameter information. + -- + --Either a string or an inclusive start and exclusive end offsets within its containing + --signature label. (see SignatureInformation.label). The offsets are based on a UTF-16 + --string representation as `Position` and `Range` does. + -- + --*Note*: a label of type string should be a substring of its containing signature label. + --Its intended use case is to highlight the parameter label part in the `SignatureInformation.label`. + label: string | [number, number]; + --The human-readable doc-comment of this parameter. Will be shown + --in the UI but can be omitted. + documentation?: string | MarkupContent; + } + --]=] + -- TODO highlight parameter + if parameter.documentation then + util.convert_input_to_markdown_lines(parameter.documentation, contents) + end + end + end + return contents +end + function M.signature_help() local params = protocol.make_text_document_position_params() - request('textDocument/signatureHelp', params, location_callback) + focusable_preview('textDocument/signatureHelp', params, function(result) + if not (result and result.signatures and result.signatures[1]) then return end + + -- TODO show empty popup when signatures is empty? + local lines = signature_help_to_preview_contents(result) + lines = util.trim_empty_lines(lines) + if vim.tbl_isempty(lines) then + return { 'No signature available' } + end + return lines, util.try_trim_markdown_code_blocks(lines) + end) end -- TODO(ashkan) ? @@ -195,4 +264,17 @@ end function M.range_formatting() end +function M.rename(new_name) + -- TODO(ashkan) use prepareRename + -- * result: [`Range`](#range) \| `{ range: Range, placeholder: string }` \| `null` describing the range of the string to rename and optionally a placeholder text of the string content to be renamed. If `null` is returned then it is deemed that a 'textDocument/rename' request is not valid at the given position. + local params = protocol.make_text_document_position_params() + new_name = new_name or npcall(vfn.input, "New Name: ") + if not (new_name and #new_name > 0) then return end + params.newName = new_name + request('textDocument/rename', params, function(_, _, result) + if not result then return end + util.workspace_apply_workspace_edit(result) + end) +end + return M diff --git a/runtime/lua/vim/lsp/builtin_callbacks.lua b/runtime/lua/vim/lsp/builtin_callbacks.lua deleted file mode 100644 index 2229547f20..0000000000 --- a/runtime/lua/vim/lsp/builtin_callbacks.lua +++ /dev/null @@ -1,344 +0,0 @@ ---- Implements the following default callbacks: --- --- vim.api.nvim_buf_set_lines(0, 0, 0, false, vim.tbl_keys(vim.lsp.builtin_callbacks)) --- - --- textDocument/completion --- textDocument/declaration --- textDocument/definition --- textDocument/hover --- textDocument/implementation --- textDocument/publishDiagnostics --- textDocument/rename --- textDocument/signatureHelp --- textDocument/typeDefinition --- TODO codeLens/resolve --- TODO completionItem/resolve --- TODO documentLink/resolve --- TODO textDocument/codeAction --- TODO textDocument/codeLens --- TODO textDocument/documentHighlight --- TODO textDocument/documentLink --- TODO textDocument/documentSymbol --- TODO textDocument/formatting --- TODO textDocument/onTypeFormatting --- TODO textDocument/rangeFormatting --- TODO textDocument/references --- window/logMessage --- window/showMessage - -local log = require 'vim.lsp.log' -local protocol = require 'vim.lsp.protocol' -local util = require 'vim.lsp.util' -local api = vim.api - -local function split_lines(value) - return vim.split(value, '\n', true) -end - -local builtin_callbacks = {} - --- textDocument/completion --- https://microsoft.github.io/language-server-protocol/specification#textDocument_completion -builtin_callbacks['textDocument/completion'] = function(_, _, result) - if not result or vim.tbl_isempty(result) then - return - end - local pos = api.nvim_win_get_cursor(0) - local row, col = pos[1], pos[2] - local line = assert(api.nvim_buf_get_lines(0, row-1, row, false)[1]) - local line_to_cursor = line:sub(col+1) - - local matches = util.text_document_completion_list_to_complete_items(result, line_to_cursor) - local match_result = vim.fn.matchstrpos(line_to_cursor, '\\k\\+$') - local match_start, match_finish = match_result[2], match_result[3] - - vim.fn.complete(col + 1 - (match_finish - match_start), matches) -end - --- textDocument/rename -builtin_callbacks['textDocument/rename'] = function(_, _, result) - if not result then return end - util.workspace_apply_workspace_edit(result) -end - -builtin_callbacks['textDocument/publishDiagnostics'] = function(_, _, result) - if not result then return end - local uri = result.uri - local bufnr = vim.uri_to_bufnr(uri) - if not bufnr then - api.nvim_err_writeln(string.format("LSP.publishDiagnostics: Couldn't find buffer for %s", uri)) - return - end - util.buf_clear_diagnostics(bufnr) - util.buf_diagnostics_save_positions(bufnr, result.diagnostics) - util.buf_diagnostics_underline(bufnr, result.diagnostics) - util.buf_diagnostics_virtual_text(bufnr, result.diagnostics) - -- util.buf_loclist(bufnr, result.diagnostics) -end - -local function focusable_popup() - local popup_win - return function(winnr) - if popup_win and api.nvim_win_is_valid(popup_win) then - if api.nvim_get_current_win() == popup_win then - api.nvim_command("wincmd p") - else - api.nvim_set_current_win(popup_win) - end - return - end - popup_win = winnr - end -end - -local hover_popup = focusable_popup() - -local function trim_empty_lines(lines) - local result = {} - for _, line in ipairs(lines) do - if #line > 0 then - table.insert(result, line) - end - end - return result -end - --- Accepts markdown lines and tries to reduce it to a filetype if it is --- just a single code block. -local function try_trim_code_blocks(lines) - local language_id = lines[1]:match("^```(.*)") - if language_id then - local has_inner_code_fence = false - for i = 2, (#lines - 1) do - local line = lines[i] - if line:sub(1,3) == '```' then - has_inner_code_fence = true - break - end - end - -- No inner code fences + starting with code fence = hooray. - if not has_inner_code_fence then - table.remove(lines, 1) - table.remove(lines) - return lines, language_id - end - end - return lines, 'markdown' -end - --- textDocument/hover --- https://microsoft.github.io/language-server-protocol/specification#textDocument_hover --- @params MarkedString | MarkedString[] | MarkupContent -builtin_callbacks['textDocument/hover'] = function(_, _, result) - if result == nil or vim.tbl_isempty(result) then - return - end - - if result.contents ~= nil then - local markdown_lines = util.convert_input_to_markdown_lines(result.contents) - markdown_lines = trim_empty_lines(markdown_lines) - if vim.tbl_isempty(markdown_lines) then - markdown_lines = { 'No information available' } - end - local _, winnr = util.open_floating_preview(try_trim_code_blocks(markdown_lines)) - hover_popup(winnr) - end -end - -builtin_callbacks['textDocument/peekDefinition'] = function(_, _, result) - if result == nil or vim.tbl_isempty(result) then return end - -- TODO(ashkan) what to do with multiple locations? - result = result[1] - local bufnr = vim.uri_to_bufnr(result.uri) - assert(bufnr) - local start = result.range.start - local finish = result.range["end"] - util.open_floating_peek_preview(bufnr, start, finish, { offset_x = 1 }) - util.open_floating_preview({"*Peek:*", string.rep(" ", finish.character - start.character + 1) }, 'markdown', { offset_y = -(finish.line - start.line) }) -end - ---- Convert SignatureHelp response to preview contents. --- https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#textDocument_signatureHelp -local function signature_help_to_preview_contents(input) - if not input.signatures then - return - end - --The active signature. If omitted or the value lies outside the range of - --`signatures` the value defaults to zero or is ignored if `signatures.length - --=== 0`. Whenever possible implementors should make an active decision about - --the active signature and shouldn't rely on a default value. - local contents = {} - local active_signature = input.activeSignature or 0 - -- If the activeSignature is not inside the valid range, then clip it. - if active_signature >= #input.signatures then - active_signature = 0 - end - local signature = input.signatures[active_signature + 1] - if not signature then - return - end - vim.list_extend(contents, split_lines(signature.label)) - if signature.documentation then - util.convert_input_to_markdown_lines(signature.documentation, contents) - end - if input.parameters then - local active_parameter = input.activeParameter or 0 - -- If the activeParameter is not inside the valid range, then clip it. - if active_parameter >= #input.parameters then - active_parameter = 0 - end - local parameter = signature.parameters and signature.parameters[active_parameter] - if parameter then - --[=[ - --Represents a parameter of a callable-signature. A parameter can - --have a label and a doc-comment. - interface ParameterInformation { - --The label of this parameter information. - -- - --Either a string or an inclusive start and exclusive end offsets within its containing - --signature label. (see SignatureInformation.label). The offsets are based on a UTF-16 - --string representation as `Position` and `Range` does. - -- - --*Note*: a label of type string should be a substring of its containing signature label. - --Its intended use case is to highlight the parameter label part in the `SignatureInformation.label`. - label: string | [number, number]; - --The human-readable doc-comment of this parameter. Will be shown - --in the UI but can be omitted. - documentation?: string | MarkupContent; - } - --]=] - -- TODO highlight parameter - if parameter.documentation then - util.convert_input_to_markdown_lines(parameter.documentation, contents) - end - end - end - return contents -end - --- textDocument/signatureHelp --- https://microsoft.github.io/language-server-protocol/specification#textDocument_signatureHelp -builtin_callbacks['textDocument/signatureHelp'] = function(_, _, result) - if result == nil or vim.tbl_isempty(result) then - return - end - - -- TODO show empty popup when signatures is empty? - if #result.signatures > 0 then - local markdown_lines = signature_help_to_preview_contents(result) - if vim.tbl_isempty(markdown_lines) then - markdown_lines = { 'No signature available' } - end - util.open_floating_preview(markdown_lines, 'markdown') - end -end - -local function update_tagstack() - local bufnr = api.nvim_get_current_buf() - local line = vim.fn.line('.') - local col = vim.fn.col('.') - local tagname = vim.fn.expand('') - local item = { bufnr = bufnr, from = { bufnr, line, col, 0 }, tagname = tagname } - local winid = vim.fn.win_getid() - local tagstack = vim.fn.gettagstack(winid) - - local action - - if tagstack.length == tagstack.curidx then - action = 'r' - tagstack.items[tagstack.curidx] = item - elseif tagstack.length > tagstack.curidx then - action = 'r' - if tagstack.curidx > 1 then - tagstack.items = table.insert(tagstack.items[tagstack.curidx - 1], item) - else - tagstack.items = { item } - end - else - action = 'a' - tagstack.items = { item } - end - - tagstack.curidx = tagstack.curidx + 1 - vim.fn.settagstack(winid, tagstack, action) -end - -local function handle_location(result) - -- We can sometimes get a list of locations, so set the first value as the - -- only value we want to handle - -- TODO(ashkan) was this correct^? We could use location lists. - if result[1] ~= nil then - result = result[1] - end - if result.uri == nil then - api.nvim_err_writeln('[LSP] Could not find a valid location') - return - end - local result_file = vim.uri_to_fname(result.uri) - local bufnr = vim.fn.bufadd(result_file) - update_tagstack() - api.nvim_set_current_buf(bufnr) - local start = result.range.start - api.nvim_win_set_cursor(0, {start.line + 1, start.character}) -end - -local function location_callback(_, method, result) - if result == nil or vim.tbl_isempty(result) then - local _ = log.info() and log.info(method, 'No location found') - return nil - end - handle_location(result) - return true -end - -local location_callbacks = { - -- https://microsoft.github.io/language-server-protocol/specification#textDocument_declaration - 'textDocument/declaration'; - -- https://microsoft.github.io/language-server-protocol/specification#textDocument_definition - 'textDocument/definition'; - -- https://microsoft.github.io/language-server-protocol/specification#textDocument_implementation - 'textDocument/implementation'; - -- https://microsoft.github.io/language-server-protocol/specification#textDocument_typeDefinition - 'textDocument/typeDefinition'; -} - -for _, location_method in ipairs(location_callbacks) do - builtin_callbacks[location_method] = location_callback -end - -local function log_message(_, _, result, client_id) - local message_type = result.type - local message = result.message - local client = vim.lsp.get_client_by_id(client_id) - local client_name = client and client.name or string.format("id=%d", client_id) - if not client then - api.nvim_err_writeln(string.format("LSP[%s] client has shut down after sending the message", client_name)) - end - if message_type == protocol.MessageType.Error then - -- Might want to not use err_writeln, - -- but displaying a message with red highlights or something - api.nvim_err_writeln(string.format("LSP[%s] %s", client_name, message)) - else - local message_type_name = protocol.MessageType[message_type] - api.nvim_out_write(string.format("LSP[%s][%s] %s\n", client_name, message_type_name, message)) - end - return result -end - -builtin_callbacks['window/showMessage'] = log_message -builtin_callbacks['window/logMessage'] = log_message - --- Add boilerplate error validation and logging for all of these. -for k, fn in pairs(builtin_callbacks) do - builtin_callbacks[k] = function(err, method, params, client_id) - local _ = log.debug() and log.debug('builtin_callback', method, { params = params, client_id = client_id, err = err }) - if err then - error(tostring(err)) - end - return fn(err, method, params, client_id) - end -end - -return builtin_callbacks --- vim:sw=2 ts=2 et diff --git a/runtime/lua/vim/lsp/default_callbacks.lua b/runtime/lua/vim/lsp/default_callbacks.lua new file mode 100644 index 0000000000..63e62075b4 --- /dev/null +++ b/runtime/lua/vim/lsp/default_callbacks.lua @@ -0,0 +1,57 @@ +local log = require 'vim.lsp.log' +local protocol = require 'vim.lsp.protocol' +local util = require 'vim.lsp.util' +local api = vim.api + +local M = {} + +M['textDocument/publishDiagnostics'] = function(_, _, result) + if not result then return end + local uri = result.uri + local bufnr = vim.uri_to_bufnr(uri) + if not bufnr then + api.nvim_err_writeln(string.format("LSP.publishDiagnostics: Couldn't find buffer for %s", uri)) + return + end + util.buf_clear_diagnostics(bufnr) + util.buf_diagnostics_save_positions(bufnr, result.diagnostics) + util.buf_diagnostics_underline(bufnr, result.diagnostics) + util.buf_diagnostics_virtual_text(bufnr, result.diagnostics) + -- util.buf_loclist(bufnr, result.diagnostics) +end + +local function log_message(_, _, result, client_id) + local message_type = result.type + local message = result.message + local client = vim.lsp.get_client_by_id(client_id) + local client_name = client and client.name or string.format("id=%d", client_id) + if not client then + api.nvim_err_writeln(string.format("LSP[%s] client has shut down after sending the message", client_name)) + end + if message_type == protocol.MessageType.Error then + -- Might want to not use err_writeln, + -- but displaying a message with red highlights or something + api.nvim_err_writeln(string.format("LSP[%s] %s", client_name, message)) + else + local message_type_name = protocol.MessageType[message_type] + api.nvim_out_write(string.format("LSP[%s][%s] %s\n", client_name, message_type_name, message)) + end + return result +end + +M['window/showMessage'] = log_message +M['window/logMessage'] = log_message + +-- Add boilerplate error validation and logging for all of these. +for k, fn in pairs(M) do + M[k] = function(err, method, params, client_id) + local _ = log.debug() and log.debug('default_callback', method, { params = params, client_id = client_id, err = err }) + if err then + error(tostring(err)) + end + return fn(err, method, params, client_id) + end +end + +return M +-- vim:sw=2 ts=2 et diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index 1413a88ce2..1f51e7bef7 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -10,7 +10,6 @@ end --[=[ -- Useful for interfacing with: --- https://github.com/microsoft/language-server-protocol/blob/gh-pages/_specifications/specification-3-14.md -- https://github.com/microsoft/language-server-protocol/raw/gh-pages/_specifications/specification-3-14.md function transform_schema_comments() nvim.command [[silent! '<,'>g/\/\*\*\|\*\/\|^$/d]] -- cgit From 03eb88848c2bea6c0c1da7acc97754d6f47b5118 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 20 Nov 2019 16:09:03 -0800 Subject: Change callback resolution to be dynamic. This allows default_callbacks to be specified after client creation to be considered. Also it simplifies the code. --- runtime/lua/vim/lsp.lua | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 44909a85ee..a284dd1ee7 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -284,19 +284,19 @@ function lsp.start_client(config) local client_id = next_client_id() - local callbacks = tbl_extend("keep", config.callbacks or {}, default_callbacks) - -- Copy metatable if it has one. - if config.callbacks and config.callbacks.__metatable then - setmetatable(callbacks, getmetatable(config.callbacks)) - end + local callbacks = config.callbacks or {} local name = config.name or tostring(client_id) local log_prefix = string.format("LSP[%s]", name) local handlers = {} + local function resolve_callback(method) + return callbacks[method] or default_callbacks[method] + end + function handlers.notification(method, params) local _ = log.debug() and log.debug('notification', method, params) - local callback = callbacks[method] + local callback = resolve_callback(method) if callback then -- Method name is provided here for convenience. callback(nil, method, params, client_id) @@ -305,7 +305,7 @@ function lsp.start_client(config) function handlers.server_request(method, params) local _ = log.debug() and log.debug('server_request', method, params) - local callback = callbacks[method] + local callback = resolve_callback(method) if callback then local _ = log.debug() and log.debug("server_request: found callback for", method) return callback(nil, method, params, client_id) @@ -316,7 +316,8 @@ function lsp.start_client(config) function handlers.on_error(code, err) local _ = log.error() and log.error(log_prefix, "on_error", { code = lsp.client_errors[code], err = err }) - nvim_err_writeln(string.format('%s: Error %s: %q', log_prefix, lsp.client_errors[code], vim.inspect(err))) + print(string.format('%s: Error %s: %q', log_prefix, lsp.client_errors[code], vim.inspect(err))) +-- nvim_err_writeln(string.format('%s: Error %s: %q', log_prefix, lsp.client_errors[code], vim.inspect(err))) if config.on_error then local status, usererr = pcall(config.on_error, code, err) if not status then @@ -440,7 +441,7 @@ function lsp.start_client(config) --- Checks capabilities before rpc.request-ing. function client.request(method, params, callback) if not callback then - callback = client.callbacks[method] + callback = resolve_callback(method) or error(string.format("request callback is empty and no default was found for client %s", client.name)) end local _ = log.debug() and log.debug(log_prefix, "client.request", client_id, method, params, callback) -- cgit From c40f8600d2418dfdfeacbba3efe11ae7c6c70ad3 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 20 Nov 2019 16:16:13 -0800 Subject: Change error writer to not be annoying. --- runtime/lua/vim/lsp.lua | 12 ++++++++---- runtime/lua/vim/lsp/buf.lua | 7 ++++++- 2 files changed, 14 insertions(+), 5 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index a284dd1ee7..1b9d788c43 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -27,6 +27,11 @@ local lsp = { -- TODO consider whether 'eol' or 'fixeol' should change the nvim_buf_get_lines that send. -- TODO improve handling of scratch buffers with LSP attached. +local function err_message(...) + nvim_err_writeln(table.concat(vim.tbl_flatten{...})) + nvim_command("redraw") +end + local function resolve_bufnr(bufnr) validate { bufnr = { bufnr, 'n', true } } if bufnr == nil or bufnr == 0 then @@ -316,13 +321,12 @@ function lsp.start_client(config) function handlers.on_error(code, err) local _ = log.error() and log.error(log_prefix, "on_error", { code = lsp.client_errors[code], err = err }) - print(string.format('%s: Error %s: %q', log_prefix, lsp.client_errors[code], vim.inspect(err))) --- nvim_err_writeln(string.format('%s: Error %s: %q', log_prefix, lsp.client_errors[code], vim.inspect(err))) + err_message(log_prefix, ': Error ', lsp.client_errors[code], ': ', vim.inspect(err)) if config.on_error then local status, usererr = pcall(config.on_error, code, err) if not status then local _ = log.error() and log.error(log_prefix, "user on_error failed", { err = usererr }) - nvim_err_writeln(log_prefix.." user on_error failed: "..tostring(usererr)) + err_message(log_prefix, ' user on_error failed: ', tostring(usererr)) end end end @@ -434,7 +438,7 @@ function lsp.start_client(config) local function unsupported_method(method) local msg = "server doesn't support "..method local _ = log.warn() and log.warn(msg) - nvim_err_writeln(msg) + err_message(msg) return lsp.rpc_response_error(protocol.ErrorCodes.MethodNotFound, msg) end diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index e8a38aa6ef..ff045cbbfc 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -23,6 +23,11 @@ local function npcall(fn, ...) return ok_or_nil(pcall(fn, ...)) end +local function err_message(...) + api.nvim_err_writeln(table.concat(vim.tbl_flatten{...})) + api.nvim_command("redraw") +end + local function find_window_by_var(name, value) for _, win in ipairs(api.nvim_list_wins()) do if npcall(api.nvim_win_get_var, win, name) == value then @@ -133,7 +138,7 @@ local function handle_location(result) result = result[1] end if result.uri == nil then - api.nvim_err_writeln('[LSP] Could not find a valid location') + err_message('[LSP] Could not find a valid location') return end local result_file = vim.uri_to_fname(result.uri) -- cgit From 1e16b3cf281bec73ccbd155dd11b2db048d1219a Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 20 Nov 2019 16:16:36 -0800 Subject: Spaces not tabs. --- runtime/lua/vim/lsp/buf.lua | 201 ++++++++++++++++++++++---------------------- 1 file changed, 101 insertions(+), 100 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index ff045cbbfc..8b21370800 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -16,11 +16,11 @@ local function resolve_bufnr(bufnr) end local function ok_or_nil(status, ...) - if not status then return end - return ... + if not status then return end + return ... end local function npcall(fn, ...) - return ok_or_nil(pcall(fn, ...)) + return ok_or_nil(pcall(fn, ...)) end local function err_message(...) @@ -29,78 +29,78 @@ local function err_message(...) end local function find_window_by_var(name, value) - for _, win in ipairs(api.nvim_list_wins()) do - if npcall(api.nvim_win_get_var, win, name) == value then - return win - end - end + for _, win in ipairs(api.nvim_list_wins()) do + if npcall(api.nvim_win_get_var, win, name) == value then + return win + end + end end local function request(method, params, callback) - -- TODO(ashkan) enable this. - -- callback = vim.lsp.default_callbacks[method] or callback - validate { - method = {method, 's'}; - callback = {callback, 'f'}; - } - return vim.lsp.buf_request(0, method, params, function(err, _, result, client_id) - if err then error(tostring(err)) end - return callback(err, method, result, client_id) - end) + -- TODO(ashkan) enable this. + -- callback = vim.lsp.default_callbacks[method] or callback + validate { + method = {method, 's'}; + callback = {callback, 'f'}; + } + return vim.lsp.buf_request(0, method, params, function(err, _, result, client_id) + if err then error(tostring(err)) end + return callback(err, method, result, client_id) + end) end local function focusable_preview(method, params, fn) - if npcall(api.nvim_win_get_var, 0, method) then - return api.nvim_command("wincmd p") - end + if npcall(api.nvim_win_get_var, 0, method) then + return api.nvim_command("wincmd p") + end - local bufnr = api.nvim_get_current_buf() - do - local win = find_window_by_var(method, bufnr) - if win then - return api.nvim_set_current_win(win) - end - end - return request(method, params, function(_, _, result, _) - -- TODO(ashkan) could show error in preview... - local lines, filetype, opts = fn(result) - if lines then - local _, winnr = util.open_floating_preview(lines, filetype, opts) - api.nvim_win_set_var(winnr, method, bufnr) - end - end) + local bufnr = api.nvim_get_current_buf() + do + local win = find_window_by_var(method, bufnr) + if win then + return api.nvim_set_current_win(win) + end + end + return request(method, params, function(_, _, result, _) + -- TODO(ashkan) could show error in preview... + local lines, filetype, opts = fn(result) + if lines then + local _, winnr = util.open_floating_preview(lines, filetype, opts) + api.nvim_win_set_var(winnr, method, bufnr) + end + end) end function M.hover() - local params = protocol.make_text_document_position_params() - focusable_preview('textDocument/hover', params, function(result) - if not (result and result.contents) then return end + local params = protocol.make_text_document_position_params() + focusable_preview('textDocument/hover', params, function(result) + if not (result and result.contents) then return end - local markdown_lines = util.convert_input_to_markdown_lines(result.contents) - markdown_lines = util.trim_empty_lines(markdown_lines) - if vim.tbl_isempty(markdown_lines) then - return { 'No information available' } - end - return markdown_lines, util.try_trim_markdown_code_blocks(markdown_lines) - end) + local markdown_lines = util.convert_input_to_markdown_lines(result.contents) + markdown_lines = util.trim_empty_lines(markdown_lines) + if vim.tbl_isempty(markdown_lines) then + return { 'No information available' } + end + return markdown_lines, util.try_trim_markdown_code_blocks(markdown_lines) + end) end function M.peek_definition() - local params = protocol.make_text_document_position_params() - request('textDocument/peekDefinition', params, function(_, _, result, _) - if not (result and result[1]) then return end - local loc = result[1] - local bufnr = vim.uri_to_bufnr(loc.uri) or error("couldn't find file "..tostring(loc.uri)) - local start = loc.range.start - local finish = loc.range["end"] - util.open_floating_peek_preview(bufnr, start, finish, { offset_x = 1 }) - local headbuf = util.open_floating_preview({"Peek:"}, nil, { - offset_y = -(finish.line - start.line); - width = finish.character - start.character + 2; - }) - -- TODO(ashkan) change highlight group? - api.nvim_buf_add_highlight(headbuf, -1, 'Keyword', 0, -1) - end) + local params = protocol.make_text_document_position_params() + request('textDocument/peekDefinition', params, function(_, _, result, _) + if not (result and result[1]) then return end + local loc = result[1] + local bufnr = vim.uri_to_bufnr(loc.uri) or error("couldn't find file "..tostring(loc.uri)) + local start = loc.range.start + local finish = loc.range["end"] + util.open_floating_peek_preview(bufnr, start, finish, { offset_x = 1 }) + local headbuf = util.open_floating_preview({"Peek:"}, nil, { + offset_y = -(finish.line - start.line); + width = finish.character - start.character + 2; + }) + -- TODO(ashkan) change highlight group? + api.nvim_buf_add_highlight(headbuf, -1, 'Keyword', 0, -1) + end) end @@ -158,23 +158,23 @@ local function location_callback(_, method, result) end function M.declaration() - local params = protocol.make_text_document_position_params() - request('textDocument/declaration', params, location_callback) + local params = protocol.make_text_document_position_params() + request('textDocument/declaration', params, location_callback) end function M.definition() - local params = protocol.make_text_document_position_params() - request('textDocument/definition', params, location_callback) + local params = protocol.make_text_document_position_params() + request('textDocument/definition', params, location_callback) end function M.type_definition() - local params = protocol.make_text_document_position_params() - request('textDocument/typeDefinition', params, location_callback) + local params = protocol.make_text_document_position_params() + request('textDocument/typeDefinition', params, location_callback) end function M.implementation() - local params = protocol.make_text_document_position_params() - request('textDocument/implementation', params, location_callback) + local params = protocol.make_text_document_position_params() + request('textDocument/implementation', params, location_callback) end --- Convert SignatureHelp response to preview contents. @@ -237,49 +237,50 @@ local function signature_help_to_preview_contents(input) end function M.signature_help() - local params = protocol.make_text_document_position_params() - focusable_preview('textDocument/signatureHelp', params, function(result) - if not (result and result.signatures and result.signatures[1]) then return end + local params = protocol.make_text_document_position_params() + focusable_preview('textDocument/signatureHelp', params, function(result) + if not (result and result.signatures and result.signatures[1]) then return end - -- TODO show empty popup when signatures is empty? - local lines = signature_help_to_preview_contents(result) - lines = util.trim_empty_lines(lines) - if vim.tbl_isempty(lines) then - return { 'No signature available' } - end - return lines, util.try_trim_markdown_code_blocks(lines) - end) + -- TODO show empty popup when signatures is empty? + local lines = signature_help_to_preview_contents(result) + lines = util.trim_empty_lines(lines) + if vim.tbl_isempty(lines) then + return { 'No signature available' } + end + return lines, util.try_trim_markdown_code_blocks(lines) + end) end -- TODO(ashkan) ? function M.completion(context) - local params = protocol.make_text_document_position_params() - params.context = context - return request('textDocument/completion', params, function(_, _, result) - if vim.tbl_isempty(result or {}) then return end - local row, col = unpack(api.nvim_win_get_cursor(0)) - local line = assert(api.nvim_buf_get_lines(0, row-1, row, false)[1]) - local line_to_cursor = line:sub(col+1) + local params = protocol.make_text_document_position_params() + params.context = context + return request('textDocument/completion', params, function(_, _, result) + if vim.tbl_isempty(result or {}) then return end + local row, col = unpack(api.nvim_win_get_cursor(0)) + local line = assert(api.nvim_buf_get_lines(0, row-1, row, false)[1]) + local line_to_cursor = line:sub(col+1) - local matches = util.text_document_completion_list_to_complete_items(result, line_to_cursor) - vim.fn.complete(col, matches) - end) + local matches = util.text_document_completion_list_to_complete_items(result, line_to_cursor) + vim.fn.complete(col, matches) + end) end function M.range_formatting() end function M.rename(new_name) - -- TODO(ashkan) use prepareRename - -- * result: [`Range`](#range) \| `{ range: Range, placeholder: string }` \| `null` describing the range of the string to rename and optionally a placeholder text of the string content to be renamed. If `null` is returned then it is deemed that a 'textDocument/rename' request is not valid at the given position. - local params = protocol.make_text_document_position_params() - new_name = new_name or npcall(vfn.input, "New Name: ") - if not (new_name and #new_name > 0) then return end - params.newName = new_name - request('textDocument/rename', params, function(_, _, result) - if not result then return end - util.workspace_apply_workspace_edit(result) - end) + -- TODO(ashkan) use prepareRename + -- * result: [`Range`](#range) \| `{ range: Range, placeholder: string }` \| `null` describing the range of the string to rename and optionally a placeholder text of the string content to be renamed. If `null` is returned then it is deemed that a 'textDocument/rename' request is not valid at the given position. + local params = protocol.make_text_document_position_params() + new_name = new_name or npcall(vfn.input, "New Name: ") + if not (new_name and #new_name > 0) then return end + params.newName = new_name + request('textDocument/rename', params, function(_, _, result) + if not result then return end + util.workspace_apply_workspace_edit(result) + end) end return M +-- vim:sw=2 ts=2 et -- cgit From 93beae4f31d42dc70c874020011220444d7f979c Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 20 Nov 2019 16:35:11 -0800 Subject: Fix rename support. --- runtime/lua/vim/lsp/buf.lua | 1 + runtime/lua/vim/lsp/util.lua | 29 +++++++++++------------------ 2 files changed, 12 insertions(+), 18 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 8b21370800..01174a1e48 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -44,6 +44,7 @@ local function request(method, params, callback) callback = {callback, 'f'}; } return vim.lsp.buf_request(0, method, params, function(err, _, result, client_id) + local _ = log.debug() and log.debug("vim.lsp.buf", method, client_id, err, result) if err then error(tostring(err)) end return callback(err, method, result, client_id) end) diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 13c83fefd6..ab2c02ffcd 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -52,16 +52,12 @@ function M.text_document_apply_text_edit(text_edit, bufnr) api.nvim_buf_set_lines(bufnr, start.line, finish.line, false, new_lines) return end - api.nvim_err_writeln('apply_text_edit currently only supports character ranges starting at 0') - error('apply_text_edit currently only supports character ranges starting at 0') - return - -- TODO test and finish this support for character ranges. --- local lines = api.nvim_buf_get_lines(0, start.line, finish.line + 1, false) --- local suffix = lines[#lines]:sub(finish.character+2) --- local prefix = lines[1]:sub(start.character+2) --- new_lines[#new_lines] = new_lines[#new_lines]..suffix --- new_lines[1] = prefix..new_lines[1] --- api.nvim_buf_set_lines(0, start.line, finish.line, false, new_lines) + local lines = api.nvim_buf_get_lines(bufnr, start.line, finish.line + 1, false) + local suffix = lines[#lines]:sub(finish.character+1) + local prefix = lines[1]:sub(1, start.character) + new_lines[#new_lines] = new_lines[#new_lines]..suffix + new_lines[1] = prefix..new_lines[1] + api.nvim_buf_set_lines(bufnr, start.line, finish.line + 1, false, new_lines) end -- textDocument/completion response returns one of CompletionItem[], CompletionList or null. @@ -158,18 +154,15 @@ function M.workspace_apply_workspace_edit(workspace_edit) return end - if workspace_edit.changes == nil or #workspace_edit.changes == 0 then + local all_changes = workspace_edit.changes + if not (all_changes and not vim.tbl_isempty(all_changes)) then return end - for uri, changes in pairs(workspace_edit.changes) do - local fname = vim.uri_to_fname(uri) - -- TODO improve this approach. Try to edit open buffers without switching. - -- Not sure how to handle files which aren't open. This is deprecated - -- anyway, so I guess it could be left as is. - api.nvim_command('edit '..fname) + for uri, changes in pairs(all_changes) do + local bufnr = vim.uri_to_bufnr(uri) for _, change in ipairs(changes) do - M.text_document_apply_text_edit(change) + M.text_document_apply_text_edit(change, bufnr) end end end -- cgit From 4c7ef3754a7679a7f095fa9e739a665ead4c71b4 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 20 Nov 2019 16:37:23 -0800 Subject: Satisfy lualint. --- runtime/lua/vim/lsp.lua | 2 +- runtime/lua/vim/lsp/buf.lua | 10 +--------- 2 files changed, 2 insertions(+), 10 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 1b9d788c43..b2dc1315dd 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -903,7 +903,7 @@ end -- Print some debug information about all LSP related things. -- The output of this function should not be relied upon and may change. function lsp.print_debug_info() - print(vim.inspect({ clients = active_clients, filetype_configs = all_filetype_configs })) + print(vim.inspect({ clients = active_clients })) end -- Log level dictionary with reverse lookup as well. diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 01174a1e48..1c2e60f1ca 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -7,14 +7,6 @@ local log = require 'vim.lsp.log' local M = {} -local function resolve_bufnr(bufnr) - validate { bufnr = { bufnr, 'n', true } } - if bufnr == nil or bufnr == 0 then - return api.nvim_get_current_buf() - end - return bufnr -end - local function ok_or_nil(status, ...) if not status then return end return ... @@ -198,7 +190,7 @@ local function signature_help_to_preview_contents(input) if not signature then return end - vim.list_extend(contents, split_lines(signature.label)) + vim.list_extend(contents, vim.split(signature.label, '\n', true)) if signature.documentation then util.convert_input_to_markdown_lines(signature.documentation, contents) end -- cgit From 9b7f95671559a9189569dcb93778e54eaa87f255 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 20 Nov 2019 16:39:45 -0800 Subject: Use err_message in default_callbacks --- runtime/lua/vim/lsp/default_callbacks.lua | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/default_callbacks.lua b/runtime/lua/vim/lsp/default_callbacks.lua index 63e62075b4..67c33a1667 100644 --- a/runtime/lua/vim/lsp/default_callbacks.lua +++ b/runtime/lua/vim/lsp/default_callbacks.lua @@ -20,18 +20,23 @@ M['textDocument/publishDiagnostics'] = function(_, _, result) -- util.buf_loclist(bufnr, result.diagnostics) end +local function err_message(...) + api.nvim_err_writeln(table.concat(vim.tbl_flatten{...})) + api.nvim_command("redraw") +end + local function log_message(_, _, result, client_id) local message_type = result.type local message = result.message local client = vim.lsp.get_client_by_id(client_id) local client_name = client and client.name or string.format("id=%d", client_id) if not client then - api.nvim_err_writeln(string.format("LSP[%s] client has shut down after sending the message", client_name)) + err_message("LSP[", client_name, "] client has shut down after sending the message") end if message_type == protocol.MessageType.Error then -- Might want to not use err_writeln, -- but displaying a message with red highlights or something - api.nvim_err_writeln(string.format("LSP[%s] %s", client_name, message)) + err_message("LSP[", client_name, "] ", message) else local message_type_name = protocol.MessageType[message_type] api.nvim_out_write(string.format("LSP[%s][%s] %s\n", client_name, message_type_name, message)) -- cgit From b984f613c1e8dadbe59bf0d7093a6ed12af61b37 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 20 Nov 2019 17:09:21 -0800 Subject: Extend list_extend to take start/finish. --- runtime/lua/vim/lsp/default_callbacks.lua | 14 ++++++-------- runtime/lua/vim/lsp/util.lua | 7 +------ runtime/lua/vim/shared.lua | 31 +++++++++++++++++++------------ 3 files changed, 26 insertions(+), 26 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/default_callbacks.lua b/runtime/lua/vim/lsp/default_callbacks.lua index 67c33a1667..5e8552307b 100644 --- a/runtime/lua/vim/lsp/default_callbacks.lua +++ b/runtime/lua/vim/lsp/default_callbacks.lua @@ -5,12 +5,17 @@ local api = vim.api local M = {} +local function err_message(...) + api.nvim_err_writeln(table.concat(vim.tbl_flatten{...})) + api.nvim_command("redraw") +end + M['textDocument/publishDiagnostics'] = function(_, _, result) if not result then return end local uri = result.uri local bufnr = vim.uri_to_bufnr(uri) if not bufnr then - api.nvim_err_writeln(string.format("LSP.publishDiagnostics: Couldn't find buffer for %s", uri)) + err_message("LSP.publishDiagnostics: Couldn't find buffer for ", uri) return end util.buf_clear_diagnostics(bufnr) @@ -20,11 +25,6 @@ M['textDocument/publishDiagnostics'] = function(_, _, result) -- util.buf_loclist(bufnr, result.diagnostics) end -local function err_message(...) - api.nvim_err_writeln(table.concat(vim.tbl_flatten{...})) - api.nvim_command("redraw") -end - local function log_message(_, _, result, client_id) local message_type = result.type local message = result.message @@ -34,8 +34,6 @@ local function log_message(_, _, result, client_id) err_message("LSP[", client_name, "] client has shut down after sending the message") end if message_type == protocol.MessageType.Error then - -- Might want to not use err_writeln, - -- but displaying a message with red highlights or something err_message("LSP[", client_name, "] ", message) else local message_type_name = protocol.MessageType[message_type] diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index ab2c02ffcd..3a2142a478 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -546,7 +546,6 @@ end -- Remove empty lines from the beginning and end. function M.trim_empty_lines(lines) - local result = {} local start = 1 for i = 1, #lines do if #lines[i] > 0 then @@ -561,11 +560,7 @@ function M.trim_empty_lines(lines) break end end - -- TODO(ashkan) use tbl_slice. - for i = start, finish do - table.insert(result, lines[i]) - end - return result + return vim.list_extend({}, lines, start, finish) end -- Accepts markdown lines and tries to reduce it to a filetype if it is diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index ff89acc524..25287ed1aa 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -226,18 +226,25 @@ function vim.tbl_add_reverse_lookup(o) return o end ---- Extends a list-like table with the values of another list-like table. ---- ---NOTE: This *mutates* dst! ---@see |extend()| ---- ---@param dst The list which will be modified and appended to. ---@param src The list from which values will be inserted. -function vim.list_extend(dst, src) - assert(type(dst) == 'table', "dst must be a table") - assert(type(src) == 'table', "src must be a table") - for _, v in ipairs(src) do - table.insert(dst, v) +-- Extends a list-like table with the values of another list-like table. +-- +-- NOTE: This *mutates* dst! +-- @see |extend()| +-- +-- @param dst list which will be modified and appended to. +-- @param src list from which values will be inserted. +-- @param start Start index on src. defaults to 1 +-- @param finish Final index on src. defaults to #src +-- @returns dst +function vim.list_extend(dst, src, start, finish) + vim.validate { + dst = {dst, 't'}; + src = {src, 't'}; + start = {start, 'n', true}; + finish = {finish, 'n', true}; + } + for i = start or 1, finish or #src do + table.insert(dst, src[i]) end return dst end -- cgit From 6fc409d5939500b038ba281ac1929fd053f35310 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 20 Nov 2019 20:51:44 -0800 Subject: Add full text_edit implementation. - Implements textDocument/formatting, textDocument/rangeFormatting, workspace/applyEdit. TODO: - still has edge cases around replacement probably. Only tested with inserts on the same position. --- runtime/lua/vim/lsp/buf.lua | 122 ++++++++++++++++++++++++++- runtime/lua/vim/lsp/default_callbacks.lua | 9 ++ runtime/lua/vim/lsp/util.lua | 134 ++++++++++++++++++++++-------- 3 files changed, 228 insertions(+), 37 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 1c2e60f1ca..ea88207b5b 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -259,7 +259,127 @@ function M.completion(context) end) end -function M.range_formatting() +local function apply_edit_to_lines(lines, start_pos, end_pos, new_lines) + -- 0-indexing to 1-indexing makes things look a bit worse. + local i_0 = start_pos[1] + 1 + local i_n = end_pos[1] + 1 + local n = i_n - i_0 + 1 + if not lines[i_0] or not lines[i_n] then + error(vim.inspect{#lines, i_0, i_n, n, start_pos, end_pos, new_lines}) + end + local prefix = "" + local suffix = lines[i_n]:sub(end_pos[2]+1) + lines[i_n] = lines[i_n]:sub(1, end_pos[2]+1) + if start_pos[2] > 0 then + prefix = lines[i_0]:sub(1, start_pos[2]) + -- lines[i_0] = lines[i_0]:sub(start.character+1) + end + -- TODO(ashkan) figure out how to avoid copy here. likely by changing algo. + new_lines = vim.list_extend({}, new_lines) + if #suffix > 0 then + new_lines[#new_lines] = new_lines[#new_lines]..suffix + end + if #prefix > 0 then + new_lines[1] = prefix..new_lines[1] + end + if #new_lines >= n then + for i = 1, n do + lines[i + i_0 - 1] = new_lines[i] + end + for i = n+1,#new_lines do + table.insert(lines, i_n + 1, new_lines[i]) + end + else + for i = 1, #new_lines do + lines[i + i_0 - 1] = new_lines[i] + end + for _ = #new_lines+1, n do + table.remove(lines, i_0 + #new_lines + 1) + end + end +end + +local function apply_text_edits(text_edits, bufnr) + if not next(text_edits) then return end + -- nvim.print("Start", #text_edits) + local start_line, finish_line = math.huge, -1 + local cleaned = {} + for _, e in ipairs(text_edits) do + start_line = math.min(e.range.start.line, start_line) + finish_line = math.max(e.range["end"].line, finish_line) + table.insert(cleaned, { + A = {e.range.start.line; e.range.start.character}; + B = {e.range["end"].line; e.range["end"].character}; + lines = vim.split(e.newText, '\n', true); + }) + end + local lines = api.nvim_buf_get_lines(bufnr, start_line, finish_line + 1, false) + for i, e in ipairs(cleaned) do + -- nvim.print(i, "e", e.A, e.B, #e.lines[#e.lines], e.lines) + local y = 0 + local x = 0 + -- TODO(ashkan) this could be done in O(n) with dynamic programming + for j = 1, i-1 do + local o = cleaned[j] + -- nvim.print(i, "o", o.A, o.B, x, y, #o.lines[#o.lines], o.lines) + if o.A[1] <= e.A[1] and o.A[2] <= e.A[2] then + y = y - (o.B[1] - o.A[1] + 1) + #o.lines + -- Same line + if #o.lines > 1 then + x = -e.A[2] + #o.lines[#o.lines] + else + if o.A[1] == e.A[1] then + -- Try to account for insertions. + -- TODO how to account for deletions? + x = x - (o.B[2] - o.A[2]) + #o.lines[#o.lines] + end + end + end + end + local A = {e.A[1] + y - start_line, e.A[2] + x} + local B = {e.B[1] + y - start_line, e.B[2] + x} + -- if x ~= 0 or y ~= 0 then + -- nvim.print(i, "_", e.A, e.B, y, x, A, B, e.lines) + -- end + apply_edit_to_lines(lines, A, B, e.lines) + end + api.nvim_buf_set_lines(bufnr, start_line, finish_line + 1, false, lines) +end + +function M.formatting(options) + validate { options = {options, 't', true} } + local params = { + textDocument = { uri = vim.uri_from_bufnr(0) }; + options = options or {}; + } + params.options[vim.type_idx] = vim.types.dictionary + return request('textDocument/formatting', params, function(_, _, result) + if not result then return end + apply_text_edits(result) + end) +end + +function M.range_formatting(options, start_pos, end_pos) + validate { + options = {options, 't', true}; + start_pos = {start_pos, 't', true}; + end_pos = {end_pos, 't', true}; + } + start_pos = start_pos or vim.api.nvim_buf_get_mark(0, '<') + end_pos = end_pos or vim.api.nvim_buf_get_mark(0, '>') + local params = { + textDocument = { uri = vim.uri_from_bufnr(0) }; + range = { + start = { line = start_pos[1]; character = start_pos[2]; }; + ["end"] = { line = end_pos[1]; character = end_pos[2]; }; + }; + options = options or {}; + } + params.options[vim.type_idx] = vim.types.dictionary + return request('textDocument/rangeFormatting', params, function(_, _, result) + if not result then return end + apply_text_edits(result) + end) end function M.rename(new_name) diff --git a/runtime/lua/vim/lsp/default_callbacks.lua b/runtime/lua/vim/lsp/default_callbacks.lua index 5e8552307b..d58280e0f2 100644 --- a/runtime/lua/vim/lsp/default_callbacks.lua +++ b/runtime/lua/vim/lsp/default_callbacks.lua @@ -10,6 +10,15 @@ local function err_message(...) api.nvim_command("redraw") end +M['workspace/applyEdit'] = function(_, _, workspace_edit) + if not workspace_edit then return end + -- TODO(ashkan) Do something more with label? + if workspace_edit.label then + print("Workspace edit", workspace_edit.label) + end + util.apply_workspace_edit(workspace_edit.edit) +end + M['textDocument/publishDiagnostics'] = function(_, _, result) if not result then return end local uri = result.uri diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 3a2142a478..004fb81cba 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -33,6 +33,93 @@ local function resolve_bufnr(bufnr) return bufnr end +function M.apply_edit_to_lines(lines, start_pos, end_pos, new_lines) + -- 0-indexing to 1-indexing makes things look a bit worse. + local i_0 = start_pos[1] + 1 + local i_n = end_pos[1] + 1 + local n = i_n - i_0 + 1 + if not lines[i_0] or not lines[i_n] then + error(vim.inspect{#lines, i_0, i_n, n, start_pos, end_pos, new_lines}) + end + local prefix = "" + local suffix = lines[i_n]:sub(end_pos[2]+1) + lines[i_n] = lines[i_n]:sub(1, end_pos[2]+1) + if start_pos[2] > 0 then + prefix = lines[i_0]:sub(1, start_pos[2]) + -- lines[i_0] = lines[i_0]:sub(start.character+1) + end + -- TODO(ashkan) figure out how to avoid copy here. likely by changing algo. + new_lines = vim.list_extend({}, new_lines) + if #suffix > 0 then + new_lines[#new_lines] = new_lines[#new_lines]..suffix + end + if #prefix > 0 then + new_lines[1] = prefix..new_lines[1] + end + if #new_lines >= n then + for i = 1, n do + lines[i + i_0 - 1] = new_lines[i] + end + for i = n+1,#new_lines do + table.insert(lines, i_n + 1, new_lines[i]) + end + else + for i = 1, #new_lines do + lines[i + i_0 - 1] = new_lines[i] + end + for _ = #new_lines+1, n do + table.remove(lines, i_0 + #new_lines + 1) + end + end +end + +function M.apply_text_edits(text_edits, bufnr) + if not next(text_edits) then return end + -- nvim.print("Start", #text_edits) + local start_line, finish_line = math.huge, -1 + local cleaned = {} + for _, e in ipairs(text_edits) do + start_line = math.min(e.range.start.line, start_line) + finish_line = math.max(e.range["end"].line, finish_line) + table.insert(cleaned, { + A = {e.range.start.line; e.range.start.character}; + B = {e.range["end"].line; e.range["end"].character}; + lines = vim.split(e.newText, '\n', true); + }) + end + local lines = api.nvim_buf_get_lines(bufnr, start_line, finish_line + 1, false) + for i, e in ipairs(cleaned) do + -- nvim.print(i, "e", e.A, e.B, #e.lines[#e.lines], e.lines) + local y = 0 + local x = 0 + -- TODO(ashkan) this could be done in O(n) with dynamic programming + for j = 1, i-1 do + local o = cleaned[j] + -- nvim.print(i, "o", o.A, o.B, x, y, #o.lines[#o.lines], o.lines) + if o.A[1] <= e.A[1] and o.A[2] <= e.A[2] then + y = y - (o.B[1] - o.A[1] + 1) + #o.lines + -- Same line + if #o.lines > 1 then + x = -e.A[2] + #o.lines[#o.lines] + else + if o.A[1] == e.A[1] then + -- Try to account for insertions. + -- TODO how to account for deletions? + x = x - (o.B[2] - o.A[2]) + #o.lines[#o.lines] + end + end + end + end + local A = {e.A[1] + y - start_line, e.A[2] + x} + local B = {e.B[1] + y - start_line, e.B[2] + x} + -- if x ~= 0 or y ~= 0 then + -- nvim.print(i, "_", e.A, e.B, y, x, A, B, e.lines) + -- end + M.apply_edit_to_lines(lines, A, B, e.lines) + end + api.nvim_buf_set_lines(bufnr, start_line, finish_line + 1, false, lines) +end + -- local valid_windows_path_characters = "[^<>:\"/\\|?*]" -- local valid_unix_path_characters = "[^/]" -- https://github.com/davidm/lua-glob-pattern @@ -40,26 +127,6 @@ end -- function M.glob_to_regex(glob) -- end ---- Apply the TextEdit response. --- @params TextEdit [table] see https://microsoft.github.io/language-server-protocol/specification -function M.text_document_apply_text_edit(text_edit, bufnr) - bufnr = resolve_bufnr(bufnr) - local range = text_edit.range - local start = range.start - local finish = range['end'] - local new_lines = split_lines(text_edit.newText) - if start.character == 0 and finish.character == 0 then - api.nvim_buf_set_lines(bufnr, start.line, finish.line, false, new_lines) - return - end - local lines = api.nvim_buf_get_lines(bufnr, start.line, finish.line + 1, false) - local suffix = lines[#lines]:sub(finish.character+1) - local prefix = lines[1]:sub(1, start.character) - new_lines[#new_lines] = new_lines[#new_lines]..suffix - new_lines[1] = prefix..new_lines[1] - api.nvim_buf_set_lines(bufnr, start.line, finish.line + 1, false, new_lines) -end - -- textDocument/completion response returns one of CompletionItem[], CompletionList or null. -- https://microsoft.github.io/language-server-protocol/specification#textDocument_completion function M.extract_completion_items(result) @@ -74,18 +141,15 @@ end --- Apply the TextDocumentEdit response. -- @params TextDocumentEdit [table] see https://microsoft.github.io/language-server-protocol/specification -function M.text_document_apply_text_document_edit(text_document_edit, bufnr) - -- local text_document = text_document_edit.textDocument - -- TODO use text_document_version? - -- local text_document_version = text_document.version - - -- TODO technically, you could do this without doing multiple buf_get/set - -- by getting the full region (smallest line and largest line) and doing - -- the edits on the buffer, and then applying the buffer at the end. - -- I'm not sure if that's better. - for _, text_edit in ipairs(text_document_edit.edits) do - M.text_document_apply_text_edit(text_edit, bufnr) +function M.apply_text_document_edit(text_document_edit) + local text_document = text_document_edit.textDocument + local bufnr = vim.uri_to_bufnr(text_document.uri) + -- TODO(ashkan) check this is correct. + if api.nvim_buf_get_changedtick(bufnr) > text_document.version then + print("Buffer ", text_document.uri, " newer than edits.") + return end + M.apply_text_edits(text_document_edit.edits, bufnr) end function M.get_current_line_to_cursor() @@ -141,14 +205,14 @@ function M.text_document_completion_list_to_complete_items(result, line_prefix) end -- @params WorkspaceEdit [table] see https://microsoft.github.io/language-server-protocol/specification -function M.workspace_apply_workspace_edit(workspace_edit) +function M.apply_workspace_edit(workspace_edit) if workspace_edit.documentChanges then for _, change in ipairs(workspace_edit.documentChanges) do if change.kind then -- TODO(ashkan) handle CreateFile/RenameFile/DeleteFile error(string.format("Unsupported change: %q", vim.inspect(change))) else - M.text_document_apply_text_document_edit(change) + M.apply_text_document_edit(change) end end return @@ -161,9 +225,7 @@ function M.workspace_apply_workspace_edit(workspace_edit) for uri, changes in pairs(all_changes) do local bufnr = vim.uri_to_bufnr(uri) - for _, change in ipairs(changes) do - M.text_document_apply_text_edit(change, bufnr) - end + M.apply_text_edits(changes, bufnr) end end -- cgit From 6d9f48ddcf3a4d0fbed0674ce180bcfb9340bb22 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 20 Nov 2019 20:57:21 -0800 Subject: Fix reference in rename. --- runtime/lua/vim/lsp/buf.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index ea88207b5b..68af34dac6 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -391,7 +391,7 @@ function M.rename(new_name) params.newName = new_name request('textDocument/rename', params, function(_, _, result) if not result then return end - util.workspace_apply_workspace_edit(result) + util.apply_workspace_edit(result) end) end -- cgit From 7bf766ad0903574b6202af0d2e11d306d656b491 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 20 Nov 2019 20:59:12 -0800 Subject: Use the apply_text_edits from util. --- runtime/lua/vim/lsp/buf.lua | 91 +-------------------------------------------- 1 file changed, 2 insertions(+), 89 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 68af34dac6..260bca281f 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -259,93 +259,6 @@ function M.completion(context) end) end -local function apply_edit_to_lines(lines, start_pos, end_pos, new_lines) - -- 0-indexing to 1-indexing makes things look a bit worse. - local i_0 = start_pos[1] + 1 - local i_n = end_pos[1] + 1 - local n = i_n - i_0 + 1 - if not lines[i_0] or not lines[i_n] then - error(vim.inspect{#lines, i_0, i_n, n, start_pos, end_pos, new_lines}) - end - local prefix = "" - local suffix = lines[i_n]:sub(end_pos[2]+1) - lines[i_n] = lines[i_n]:sub(1, end_pos[2]+1) - if start_pos[2] > 0 then - prefix = lines[i_0]:sub(1, start_pos[2]) - -- lines[i_0] = lines[i_0]:sub(start.character+1) - end - -- TODO(ashkan) figure out how to avoid copy here. likely by changing algo. - new_lines = vim.list_extend({}, new_lines) - if #suffix > 0 then - new_lines[#new_lines] = new_lines[#new_lines]..suffix - end - if #prefix > 0 then - new_lines[1] = prefix..new_lines[1] - end - if #new_lines >= n then - for i = 1, n do - lines[i + i_0 - 1] = new_lines[i] - end - for i = n+1,#new_lines do - table.insert(lines, i_n + 1, new_lines[i]) - end - else - for i = 1, #new_lines do - lines[i + i_0 - 1] = new_lines[i] - end - for _ = #new_lines+1, n do - table.remove(lines, i_0 + #new_lines + 1) - end - end -end - -local function apply_text_edits(text_edits, bufnr) - if not next(text_edits) then return end - -- nvim.print("Start", #text_edits) - local start_line, finish_line = math.huge, -1 - local cleaned = {} - for _, e in ipairs(text_edits) do - start_line = math.min(e.range.start.line, start_line) - finish_line = math.max(e.range["end"].line, finish_line) - table.insert(cleaned, { - A = {e.range.start.line; e.range.start.character}; - B = {e.range["end"].line; e.range["end"].character}; - lines = vim.split(e.newText, '\n', true); - }) - end - local lines = api.nvim_buf_get_lines(bufnr, start_line, finish_line + 1, false) - for i, e in ipairs(cleaned) do - -- nvim.print(i, "e", e.A, e.B, #e.lines[#e.lines], e.lines) - local y = 0 - local x = 0 - -- TODO(ashkan) this could be done in O(n) with dynamic programming - for j = 1, i-1 do - local o = cleaned[j] - -- nvim.print(i, "o", o.A, o.B, x, y, #o.lines[#o.lines], o.lines) - if o.A[1] <= e.A[1] and o.A[2] <= e.A[2] then - y = y - (o.B[1] - o.A[1] + 1) + #o.lines - -- Same line - if #o.lines > 1 then - x = -e.A[2] + #o.lines[#o.lines] - else - if o.A[1] == e.A[1] then - -- Try to account for insertions. - -- TODO how to account for deletions? - x = x - (o.B[2] - o.A[2]) + #o.lines[#o.lines] - end - end - end - end - local A = {e.A[1] + y - start_line, e.A[2] + x} - local B = {e.B[1] + y - start_line, e.B[2] + x} - -- if x ~= 0 or y ~= 0 then - -- nvim.print(i, "_", e.A, e.B, y, x, A, B, e.lines) - -- end - apply_edit_to_lines(lines, A, B, e.lines) - end - api.nvim_buf_set_lines(bufnr, start_line, finish_line + 1, false, lines) -end - function M.formatting(options) validate { options = {options, 't', true} } local params = { @@ -355,7 +268,7 @@ function M.formatting(options) params.options[vim.type_idx] = vim.types.dictionary return request('textDocument/formatting', params, function(_, _, result) if not result then return end - apply_text_edits(result) + util.apply_text_edits(result) end) end @@ -378,7 +291,7 @@ function M.range_formatting(options, start_pos, end_pos) params.options[vim.type_idx] = vim.types.dictionary return request('textDocument/rangeFormatting', params, function(_, _, result) if not result then return end - apply_text_edits(result) + util.apply_text_edits(result) end) end -- cgit From b7170f2d722cee24a62eb74ac836d6192e5475dc Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Wed, 20 Nov 2019 20:59:52 -0800 Subject: Remove resolve_bufnr/lualint --- runtime/lua/vim/lsp/util.lua | 7 ------- 1 file changed, 7 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 004fb81cba..dcf2c17df4 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -26,13 +26,6 @@ local function remove_prefix(prefix, word) return word:sub(prefix_length + 1) end -local function resolve_bufnr(bufnr) - if bufnr == nil or bufnr == 0 then - return api.nvim_get_current_buf() - end - return bufnr -end - function M.apply_edit_to_lines(lines, start_pos, end_pos, new_lines) -- 0-indexing to 1-indexing makes things look a bit worse. local i_0 = start_pos[1] + 1 -- cgit From 76e0a8bd935b27270f8f6a594c3025bc494f4422 Mon Sep 17 00:00:00 2001 From: Björn Linse Date: Thu, 21 Nov 2019 10:29:54 +0100 Subject: lsp: transmit "\n" after last line when 'eol' is set Otherwise some servers like clangd will emit spurious "no newline at end of file" warnings. --- runtime/lua/vim/lsp.lua | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 9dbe03dace..0c1a145c04 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -23,7 +23,6 @@ local lsp = { -- format_rpc_error = lsp_rpc.format_rpc_error; } --- TODO consider whether 'eol' or 'fixeol' should change the nvim_buf_get_lines that send. -- TODO improve handling of scratch buffers with LSP attached. local function resolve_bufnr(bufnr) @@ -175,6 +174,14 @@ local function validate_client_config(config) } end +local function buf_get_full_text(bufnr) + local text = table.concat(nvim_buf_get_lines(bufnr, 0, -1, true), '\n') + if nvim_buf_get_option(bufnr, 'eol') then + text = text .. '\n' + end + return text +end + local function text_document_did_open_handler(bufnr, client) if not client.resolved_capabilities.text_document_open_close then return @@ -188,7 +195,7 @@ local function text_document_did_open_handler(bufnr, client) uri = vim.uri_from_bufnr(bufnr); -- TODO make sure our filetypes are compatible with languageId names. languageId = nvim_buf_get_option(bufnr, 'filetype'); - text = table.concat(nvim_buf_get_lines(bufnr, 0, -1, false), '\n'); + text = buf_get_full_text(bufnr); } } client.notify('textDocument/didOpen', params) @@ -551,7 +558,7 @@ do end) local full_changes = once(function() return { - text = table.concat(nvim_buf_get_lines(bufnr, 0, -1, false), "\n"); + text = buf_get_full_text(bufnr); }; end) local uri = vim.uri_from_bufnr(bufnr) -- cgit From bcae04f6c62b23104e85bb08c54a16d0cdc33852 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Thu, 21 Nov 2019 15:19:06 -0800 Subject: Updates - Use correct implementation of text_edits. - Send indent options to rangeFormatting and formatting. - Remove references to vim bindings and filetype from lsp.txt - Add more examples to docs. - Add before_init to allow changing initialize_params. --- runtime/lua/vim/lsp.lua | 13 ++++- runtime/lua/vim/lsp/buf.lua | 14 ++++-- runtime/lua/vim/lsp/util.lua | 110 ++++++++++++++++++++----------------------- 3 files changed, 73 insertions(+), 64 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 5a0b320f4c..790f8f8a9d 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -160,13 +160,13 @@ local function validate_client_config(config) root_dir = { config.root_dir, is_dir, "directory" }; callbacks = { config.callbacks, "t", true }; capabilities = { config.capabilities, "t", true }; - -- cmd = { config.cmd, "s", false }; cmd_cwd = { config.cmd_cwd, optional_validator(is_dir), "directory" }; cmd_env = { config.cmd_env, "f", true }; name = { config.name, 's', true }; on_error = { config.on_error, "f", true }; on_exit = { config.on_exit, "f", true }; on_init = { config.on_init, "f", true }; + before_init = { config.before_init, "f", true }; offset_encoding = { config.offset_encoding, "s", true }; } local cmd, cmd_args = validate_command(config.cmd) @@ -267,6 +267,12 @@ end -- possible errors. `vim.lsp.client_errors[code]` can be used to retrieve a -- human understandable string. -- +-- before_init(initialize_params, config): A function which is called *before* +-- the request `initialize` is completed. `initialize_params` contains +-- the parameters we are sending to the server and `config` is the config that +-- was passed to `start_client()` for convenience. You can use this to modify +-- parameters before they are sent. +-- -- on_init(client, initialize_result): A function which is called after the -- request `initialize` is completed. `initialize_result` contains -- `capabilities` and anything else the server may send. For example, `clangd` @@ -385,7 +391,6 @@ function lsp.start_client(config) -- The rootUri of the workspace. Is null if no folder is open. If both -- `rootPath` and `rootUri` are set `rootUri` wins. rootUri = vim.uri_from_fname(config.root_dir); --- rootUri = vim.uri_from_fname(vim.fn.expand("%:p:h")); -- User provided initialization options. initializationOptions = config.init_options; -- The capabilities provided by the client (editor or tool) @@ -409,6 +414,10 @@ function lsp.start_client(config) -- } workspaceFolders = nil; } + if config.before_init then + -- TODO(ashkan) handle errors here. + pcall(config.before_init, initialize_params, config) + end local _ = log.debug() and log.debug(log_prefix, "initialize_params", initialize_params) rpc.request('initialize', initialize_params, function(init_err, result) assert(not init_err, tostring(init_err)) diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 260bca281f..dbc141a906 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -261,11 +261,14 @@ end function M.formatting(options) validate { options = {options, 't', true} } + options = vim.tbl_extend('keep', options or {}, { + tabSize = api.nvim_buf_get_option(0, 'tabstop'); + insertSpaces = api.nvim_buf_get_option(0, 'expandtab'); + }) local params = { textDocument = { uri = vim.uri_from_bufnr(0) }; - options = options or {}; + options = options; } - params.options[vim.type_idx] = vim.types.dictionary return request('textDocument/formatting', params, function(_, _, result) if not result then return end util.apply_text_edits(result) @@ -278,6 +281,10 @@ function M.range_formatting(options, start_pos, end_pos) start_pos = {start_pos, 't', true}; end_pos = {end_pos, 't', true}; } + options = vim.tbl_extend('keep', options or {}, { + tabSize = api.nvim_buf_get_option(0, 'tabstop'); + insertSpaces = api.nvim_buf_get_option(0, 'expandtab'); + }) start_pos = start_pos or vim.api.nvim_buf_get_mark(0, '<') end_pos = end_pos or vim.api.nvim_buf_get_mark(0, '>') local params = { @@ -286,9 +293,8 @@ function M.range_formatting(options, start_pos, end_pos) start = { line = start_pos[1]; character = start_pos[2]; }; ["end"] = { line = end_pos[1]; character = end_pos[2]; }; }; - options = options or {}; + options = options; } - params.options[vim.type_idx] = vim.types.dictionary return request('textDocument/rangeFormatting', params, function(_, _, result) if not result then return end util.apply_text_edits(result) diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index dcf2c17df4..66430fffc7 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -26,89 +26,83 @@ local function remove_prefix(prefix, word) return word:sub(prefix_length + 1) end -function M.apply_edit_to_lines(lines, start_pos, end_pos, new_lines) - -- 0-indexing to 1-indexing makes things look a bit worse. - local i_0 = start_pos[1] + 1 - local i_n = end_pos[1] + 1 - local n = i_n - i_0 + 1 - if not lines[i_0] or not lines[i_n] then - error(vim.inspect{#lines, i_0, i_n, n, start_pos, end_pos, new_lines}) +-- TODO(ashkan) @performance this could do less copying. +function M.set_lines(lines, A, B, new_lines) + -- 0-indexing to 1-indexing + local i_0 = A[1] + 1 + local i_n = B[1] + 1 + if not (i_0 >= 1 and i_0 <= #lines and i_n >= 1 and i_n <= #lines) then + error("Invalid range: "..vim.inspect{A = A; B = B; #lines, new_lines}) end local prefix = "" - local suffix = lines[i_n]:sub(end_pos[2]+1) - lines[i_n] = lines[i_n]:sub(1, end_pos[2]+1) - if start_pos[2] > 0 then - prefix = lines[i_0]:sub(1, start_pos[2]) - -- lines[i_0] = lines[i_0]:sub(start.character+1) - end - -- TODO(ashkan) figure out how to avoid copy here. likely by changing algo. - new_lines = vim.list_extend({}, new_lines) + local suffix = lines[i_n]:sub(B[2]+1) + if A[2] > 0 then + prefix = lines[i_0]:sub(1, A[2]) + end + new_lines = list_extend({}, new_lines) if #suffix > 0 then new_lines[#new_lines] = new_lines[#new_lines]..suffix end if #prefix > 0 then new_lines[1] = prefix..new_lines[1] end - if #new_lines >= n then - for i = 1, n do - lines[i + i_0 - 1] = new_lines[i] - end - for i = n+1,#new_lines do - table.insert(lines, i_n + 1, new_lines[i]) - end - else - for i = 1, #new_lines do - lines[i + i_0 - 1] = new_lines[i] - end - for _ = #new_lines+1, n do - table.remove(lines, i_0 + #new_lines + 1) + local result = list_extend({}, lines, 1, i_0 - 1) + list_extend(result, new_lines) + list_extend(result, lines, i_n + 1) + return result +end + +local function sort_by_key(fn) + return function(a,b) + local ka, kb = fn(a), fn(b) + assert(#ka == #kb) + for i = 1, #ka do + if ka[i] ~= kb[i] then + return ka[i] < kb[i] + end end + -- every value must have been equal here, which means it's not less than. + return false end end +local edit_sort_key = sort_by_key(function(e) + return {e.A[1], e.A[2], e.i} +end) function M.apply_text_edits(text_edits, bufnr) if not next(text_edits) then return end - -- nvim.print("Start", #text_edits) local start_line, finish_line = math.huge, -1 local cleaned = {} - for _, e in ipairs(text_edits) do + for i, e in ipairs(text_edits) do start_line = math.min(e.range.start.line, start_line) finish_line = math.max(e.range["end"].line, finish_line) + -- TODO(ashkan) sanity check ranges for overlap. table.insert(cleaned, { + i = i; A = {e.range.start.line; e.range.start.character}; B = {e.range["end"].line; e.range["end"].character}; lines = vim.split(e.newText, '\n', true); }) end + + -- Reverse sort the orders so we can apply them without interfering with + -- eachother. Also add i as a sort key to mimic a stable sort. + table.sort(cleaned, edit_sort_key) local lines = api.nvim_buf_get_lines(bufnr, start_line, finish_line + 1, false) - for i, e in ipairs(cleaned) do - -- nvim.print(i, "e", e.A, e.B, #e.lines[#e.lines], e.lines) - local y = 0 - local x = 0 - -- TODO(ashkan) this could be done in O(n) with dynamic programming - for j = 1, i-1 do - local o = cleaned[j] - -- nvim.print(i, "o", o.A, o.B, x, y, #o.lines[#o.lines], o.lines) - if o.A[1] <= e.A[1] and o.A[2] <= e.A[2] then - y = y - (o.B[1] - o.A[1] + 1) + #o.lines - -- Same line - if #o.lines > 1 then - x = -e.A[2] + #o.lines[#o.lines] - else - if o.A[1] == e.A[1] then - -- Try to account for insertions. - -- TODO how to account for deletions? - x = x - (o.B[2] - o.A[2]) + #o.lines[#o.lines] - end - end - end - end - local A = {e.A[1] + y - start_line, e.A[2] + x} - local B = {e.B[1] + y - start_line, e.B[2] + x} - -- if x ~= 0 or y ~= 0 then - -- nvim.print(i, "_", e.A, e.B, y, x, A, B, e.lines) - -- end - M.apply_edit_to_lines(lines, A, B, e.lines) + local fix_eol = api.nvim_buf_get_option(bufnr, 'fixeol') + local set_eol = fix_eol and api.nvim_buf_line_count(bufnr) == finish_line + 1 + if set_eol and #lines[#lines] ~= 0 then + table.insert(lines, '') + end + + for i = #cleaned, 1, -1 do + local e = cleaned[i] + local A = {e.A[1] - start_line, e.A[2]} + local B = {e.B[1] - start_line, e.B[2]} + lines = M.set_lines(lines, A, B, e.lines) + end + if set_eol and #lines[#lines] == 0 then + table.remove(lines) end api.nvim_buf_set_lines(bufnr, start_line, finish_line + 1, false, lines) end -- cgit From b27965538dcc3faa8e1c5b3d64a98108c18981ec Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Thu, 21 Nov 2019 15:20:32 -0800 Subject: Fix hovers staying on bufhidden --- runtime/lua/vim/lsp/util.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 66430fffc7..77f2ae6fbc 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -335,7 +335,7 @@ function M.open_floating_preview(contents, filetype, opts) end api.nvim_buf_set_lines(floating_bufnr, 0, -1, true, contents) api.nvim_buf_set_option(floating_bufnr, 'modifiable', false) - api.nvim_command("autocmd CursorMoved ++once lua pcall(vim.api.nvim_win_close, "..floating_winnr..", true)") + api.nvim_command("autocmd CursorMoved,BufHidden ++once lua pcall(vim.api.nvim_win_close, "..floating_winnr..", true)") return floating_bufnr, floating_winnr end -- cgit From da07e71b189a51008a93783b5f7894dc495c277e Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Thu, 21 Nov 2019 15:34:28 -0800 Subject: Account for character length in jump position. --- runtime/lua/vim/lsp/buf.lua | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index dbc141a906..0b4ac6ca9b 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -134,12 +134,14 @@ local function handle_location(result) err_message('[LSP] Could not find a valid location') return end - local result_file = vim.uri_to_fname(result.uri) - local bufnr = vfn.bufadd(result_file) + local bufnr = vim.uri_to_bufnr(result.uri) update_tagstack() api.nvim_set_current_buf(bufnr) - local start = result.range.start - api.nvim_win_set_cursor(0, {start.line + 1, start.character}) + local row = result.range.start.line + local col = result.range.start.character + local line = api.nvim_buf_get_lines(0, row, row+1, true)[1] + col = #line:sub(1, col) + api.nvim_win_set_cursor(0, {row + 1, col}) return true end local function location_callback(_, method, result) -- cgit From 43d73ee884f97861c44adab4797a8fc185af1436 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Thu, 21 Nov 2019 15:41:32 -0800 Subject: Fix position params for encoding. --- runtime/lua/vim/lsp/buf.lua | 18 +++++++++--------- runtime/lua/vim/lsp/protocol.lua | 13 ------------- runtime/lua/vim/lsp/util.lua | 11 +++++++++++ 3 files changed, 20 insertions(+), 22 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 0b4ac6ca9b..ed26e80c3c 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -65,7 +65,7 @@ local function focusable_preview(method, params, fn) end function M.hover() - local params = protocol.make_text_document_position_params() + local params = util.make_position_params() focusable_preview('textDocument/hover', params, function(result) if not (result and result.contents) then return end @@ -79,7 +79,7 @@ function M.hover() end function M.peek_definition() - local params = protocol.make_text_document_position_params() + local params = util.make_position_params() request('textDocument/peekDefinition', params, function(_, _, result, _) if not (result and result[1]) then return end local loc = result[1] @@ -153,22 +153,22 @@ local function location_callback(_, method, result) end function M.declaration() - local params = protocol.make_text_document_position_params() + local params = util.make_position_params() request('textDocument/declaration', params, location_callback) end function M.definition() - local params = protocol.make_text_document_position_params() + local params = util.make_position_params() request('textDocument/definition', params, location_callback) end function M.type_definition() - local params = protocol.make_text_document_position_params() + local params = util.make_position_params() request('textDocument/typeDefinition', params, location_callback) end function M.implementation() - local params = protocol.make_text_document_position_params() + local params = util.make_position_params() request('textDocument/implementation', params, location_callback) end @@ -232,7 +232,7 @@ local function signature_help_to_preview_contents(input) end function M.signature_help() - local params = protocol.make_text_document_position_params() + local params = util.make_position_params() focusable_preview('textDocument/signatureHelp', params, function(result) if not (result and result.signatures and result.signatures[1]) then return end @@ -248,7 +248,7 @@ end -- TODO(ashkan) ? function M.completion(context) - local params = protocol.make_text_document_position_params() + local params = util.make_position_params() params.context = context return request('textDocument/completion', params, function(_, _, result) if vim.tbl_isempty(result or {}) then return end @@ -306,7 +306,7 @@ end function M.rename(new_name) -- TODO(ashkan) use prepareRename -- * result: [`Range`](#range) \| `{ range: Range, placeholder: string }` \| `null` describing the range of the string to rename and optionally a placeholder text of the string content to be renamed. If `null` is returned then it is deemed that a 'textDocument/rename' request is not valid at the given position. - local params = protocol.make_text_document_position_params() + local params = util.make_position_params() new_name = new_name or npcall(vfn.input, "New Name: ") if not (new_name and #new_name > 0) then return end params.newName = new_name diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index 1f51e7bef7..ead90cc75a 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -680,19 +680,6 @@ function protocol.make_client_capabilities() } end -function protocol.make_text_document_position_params() - local position = vim.api.nvim_win_get_cursor(0) - return { - textDocument = { - uri = vim.uri_from_bufnr() - }; - position = { - line = position[1] - 1; - character = position[2]; - } - } -end - --[=[ export interface DocumentFilter { --A language id, like `typescript`. diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 77f2ae6fbc..76681920bd 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -638,6 +638,17 @@ function M.try_trim_markdown_code_blocks(lines) return 'markdown' end +function M.make_position_params() + local row, col = unpack(api.nvim_win_get_cursor(0)) + row = row - 1 + local line = api.nvim_buf_get_lines(0, row, row+1, true)[1] + col = vim.str_utfindex(line, col) + return { + textDocument = { uri = vim.uri_from_bufnr(0) }; + position = { line = row; character = col; } + } +end + return M -- vim:sw=2 ts=2 et -- cgit From c055ca00ce61499e9161b8d2fa48568e833ee5b0 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Thu, 21 Nov 2019 15:55:45 -0800 Subject: Remove comments. --- runtime/lua/vim/lsp.lua | 4 ---- 1 file changed, 4 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 790f8f8a9d..17d6001496 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -98,11 +98,7 @@ local function for_each_buffer_client(bufnr, callback) return end for client_id in pairs(client_ids) do - -- This is unlikely to happen. Could only potentially happen in a race - -- condition between literally a single statement. - -- We could skip this error, but let's error for now. local client = active_clients[client_id] - -- or error(string.format("Client %d has already shut down.", client_id)) if client then callback(client, client_id) end -- cgit From a3d67dac5f9de6099890ae4b9fab2b70b2279a29 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Thu, 21 Nov 2019 16:23:12 -0800 Subject: Fix encoding translation in other places. --- runtime/lua/vim/lsp.lua | 2 +- runtime/lua/vim/lsp/buf.lua | 22 ++++++++++++++++------ runtime/lua/vim/lsp/util.lua | 7 +++++++ 3 files changed, 24 insertions(+), 7 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 17d6001496..880d811647 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -862,7 +862,7 @@ function lsp.omnifunc(findstart, base) position = { -- 0-indexed for both line and character line = pos[1] - 1, - character = pos[2], + character = vim.str_utfindex(line, pos[2]), }; -- The completion context. This is only available if the client specifies -- to send this using `ClientCapabilities.textDocument.completion.contextSupport === true` diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index ed26e80c3c..b4e0b9cbfc 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -2,8 +2,8 @@ local validate = vim.validate local api = vim.api local vfn = vim.fn local util = require 'vim.lsp.util' -local protocol = require 'vim.lsp.protocol' local log = require 'vim.lsp.log' +local list_extend = vim.list_extend local M = {} @@ -192,7 +192,7 @@ local function signature_help_to_preview_contents(input) if not signature then return end - vim.list_extend(contents, vim.split(signature.label, '\n', true)) + list_extend(contents, vim.split(signature.label, '\n', true)) if signature.documentation then util.convert_input_to_markdown_lines(signature.documentation, contents) end @@ -287,13 +287,23 @@ function M.range_formatting(options, start_pos, end_pos) tabSize = api.nvim_buf_get_option(0, 'tabstop'); insertSpaces = api.nvim_buf_get_option(0, 'expandtab'); }) - start_pos = start_pos or vim.api.nvim_buf_get_mark(0, '<') - end_pos = end_pos or vim.api.nvim_buf_get_mark(0, '>') + local A = list_extend({}, start_pos or api.nvim_buf_get_mark(0, '<')) + local B = list_extend({}, end_pos or api.nvim_buf_get_mark(0, '>')) + -- convert to 0-index + A[1] = A[1] - 1 + B[1] = B[1] - 1 + -- account for encoding. + if A[2] > 0 then + A = {A[1], util.character_offset(0, unpack(A))} + end + if B[2] > 0 then + B = {B[1], util.character_offset(0, unpack(B))} + end local params = { textDocument = { uri = vim.uri_from_bufnr(0) }; range = { - start = { line = start_pos[1]; character = start_pos[2]; }; - ["end"] = { line = end_pos[1]; character = end_pos[2]; }; + start = { line = A[1]; character = A[2]; }; + ["end"] = { line = B[1]; character = B[2]; }; }; options = options; } diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 76681920bd..570c4df1dd 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -649,6 +649,13 @@ function M.make_position_params() } end +-- @param buf buffer handle or 0 for current. +-- @param row 0-indexed line +-- @param col 0-indexed byte offset in line +function M.character_offset(buf, row, col) + local line = api.nvim_buf_get_lines(buf, row, row+1, true)[1] + return vim.str_utfindex(line, col) +end return M -- vim:sw=2 ts=2 et -- cgit From 78991ffbf4357ba1ad477a13991078bb4a0bdc58 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Thu, 21 Nov 2019 23:58:32 -0800 Subject: Improve performance of util.set_lines + bugfix Also permit character_offset for col past the end of line (useful in range formatting). --- runtime/lua/vim/lsp/util.lua | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 570c4df1dd..94b4223a20 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -11,6 +11,14 @@ end local list_extend = vim.list_extend +local function ok_or_nil(status, ...) + if not status then return end + return ... +end +local function npcall(fn, ...) + return ok_or_nil(pcall(fn, ...)) +end + --- Find the longest shared prefix between prefix and word. -- e.g. remove_prefix("123tes", "testing") == "ting" local function remove_prefix(prefix, word) @@ -39,17 +47,22 @@ function M.set_lines(lines, A, B, new_lines) if A[2] > 0 then prefix = lines[i_0]:sub(1, A[2]) end - new_lines = list_extend({}, new_lines) + local n = i_n - i_0 + 1 + if n ~= #new_lines then + for _ = 1, n - #new_lines do table.remove(lines, i_0) end + for _ = 1, #new_lines - n do table.insert(lines, i_0, '') end + end + for i = 1, #new_lines do + lines[i - 1 + i_0] = new_lines[i] + end if #suffix > 0 then - new_lines[#new_lines] = new_lines[#new_lines]..suffix + local i = i_0 + #new_lines - 1 + lines[i] = lines[i]..suffix end if #prefix > 0 then - new_lines[1] = prefix..new_lines[1] + lines[i_0] = prefix..lines[i_0] end - local result = list_extend({}, lines, 1, i_0 - 1) - list_extend(result, new_lines) - list_extend(result, lines, i_n + 1) - return result + return lines end local function sort_by_key(fn) @@ -99,7 +112,7 @@ function M.apply_text_edits(text_edits, bufnr) local e = cleaned[i] local A = {e.A[1] - start_line, e.A[2]} local B = {e.B[1] - start_line, e.B[2]} - lines = M.set_lines(lines, A, B, e.lines) + M.set_lines(lines, A, B, e.lines) end if set_eol and #lines[#lines] == 0 then table.remove(lines) @@ -638,11 +651,12 @@ function M.try_trim_markdown_code_blocks(lines) return 'markdown' end +local str_utfindex = vim.str_utfindex function M.make_position_params() local row, col = unpack(api.nvim_win_get_cursor(0)) row = row - 1 local line = api.nvim_buf_get_lines(0, row, row+1, true)[1] - col = vim.str_utfindex(line, col) + col = str_utfindex(line, col) return { textDocument = { uri = vim.uri_from_bufnr(0) }; position = { line = row; character = col; } @@ -654,7 +668,9 @@ end -- @param col 0-indexed byte offset in line function M.character_offset(buf, row, col) local line = api.nvim_buf_get_lines(buf, row, row+1, true)[1] - return vim.str_utfindex(line, col) + -- TODO(ashkan) is there a better way to handle col being past line length? + -- If the col is past the EOL, use the line length. + return npcall(str_utfindex, line, col) or str_utfindex(line) end return M -- cgit From 73487f4130581da72c9e838189aab39c79c177c5 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Fri, 22 Nov 2019 00:31:10 -0800 Subject: Improve the character_offset code. --- runtime/lua/vim/lsp/util.lua | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 94b4223a20..e2a8748a20 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -11,14 +11,6 @@ end local list_extend = vim.list_extend -local function ok_or_nil(status, ...) - if not status then return end - return ... -end -local function npcall(fn, ...) - return ok_or_nil(pcall(fn, ...)) -end - --- Find the longest shared prefix between prefix and word. -- e.g. remove_prefix("123tes", "testing") == "ting" local function remove_prefix(prefix, word) @@ -668,9 +660,11 @@ end -- @param col 0-indexed byte offset in line function M.character_offset(buf, row, col) local line = api.nvim_buf_get_lines(buf, row, row+1, true)[1] - -- TODO(ashkan) is there a better way to handle col being past line length? -- If the col is past the EOL, use the line length. - return npcall(str_utfindex, line, col) or str_utfindex(line) + if col > #line then + return str_utfindex(line) + end + return str_utfindex(line, col) end return M -- cgit From d410812311f7b462420690455914ea1316953b3a Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Sat, 23 Nov 2019 16:14:24 -0800 Subject: UI tweaks. - Hide diagnostics on client exit - Stop insert on popup focus. - Hide popup on insertchar (for signature_help) --- runtime/lua/vim/lsp.lua | 12 +++++++++++- runtime/lua/vim/lsp/buf.lua | 8 ++++++-- runtime/lua/vim/lsp/util.lua | 5 +++-- 3 files changed, 20 insertions(+), 5 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 880d811647..7076db68d3 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -343,9 +343,19 @@ function lsp.start_client(config) function handlers.on_exit(code, signal) active_clients[client_id] = nil uninitialized_clients[client_id] = nil - for _, client_ids in pairs(all_buffer_active_clients) do + local active_buffers = {} + for bufnr, client_ids in pairs(all_buffer_active_clients) do + if client_ids[client_id] then + table.insert(active_buffers, bufnr) + end client_ids[client_id] = nil end + -- Buffer level cleanup + vim.schedule(function() + for _, bufnr in ipairs(active_buffers) do + util.buf_clear_diagnostics(bufnr) + end + end) if config.on_exit then pcall(config.on_exit, code, signal, client_id) end diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index b4e0b9cbfc..79d43fda4a 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -51,7 +51,9 @@ local function focusable_preview(method, params, fn) do local win = find_window_by_var(method, bufnr) if win then - return api.nvim_set_current_win(win) + api.nvim_set_current_win(win) + api.nvim_command("stopinsert") + return end end return request(method, params, function(_, _, result, _) @@ -234,7 +236,9 @@ end function M.signature_help() local params = util.make_position_params() focusable_preview('textDocument/signatureHelp', params, function(result) - if not (result and result.signatures and result.signatures[1]) then return end + if not (result and result.signatures and result.signatures[1]) then + return { 'No signature available' } + end -- TODO show empty popup when signatures is empty? local lines = signature_help_to_preview_contents(result) diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index e2a8748a20..0c53494f02 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -104,7 +104,7 @@ function M.apply_text_edits(text_edits, bufnr) local e = cleaned[i] local A = {e.A[1] - start_line, e.A[2]} local B = {e.B[1] - start_line, e.B[2]} - M.set_lines(lines, A, B, e.lines) + lines = M.set_lines(lines, A, B, e.lines) end if set_eol and #lines[#lines] == 0 then table.remove(lines) @@ -340,7 +340,8 @@ function M.open_floating_preview(contents, filetype, opts) end api.nvim_buf_set_lines(floating_bufnr, 0, -1, true, contents) api.nvim_buf_set_option(floating_bufnr, 'modifiable', false) - api.nvim_command("autocmd CursorMoved,BufHidden ++once lua pcall(vim.api.nvim_win_close, "..floating_winnr..", true)") + -- TODO make InsertCharPre disappearing optional? + api.nvim_command("autocmd CursorMoved,BufHidden,InsertCharPre ++once lua pcall(vim.api.nvim_win_close, "..floating_winnr..", true)") return floating_bufnr, floating_winnr end -- cgit From d0d38fc36e0c1602186aa540417070fa6c1e2746 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Sun, 24 Nov 2019 02:28:48 -0800 Subject: Lua: vim.env, vim.{g,v,w,bo,wo} #11442 - Add vim variable meta accessors: vim.env, vim.{g,v,w,bo,wo} - Redo gen_char_blob to generate multiple blobs instead of just one so that multiple Lua modules can be inlined. - Reorder vim.lua inclusion so that it can use previously defined C functions and utility functions like vim.shared and vim.inspect things. - Inline shared.lua into nvim, but also keep it available in runtime. --- runtime/lua/vim/shared.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index ff89acc524..6aa5f3bd9b 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -4,7 +4,7 @@ -- test-suite. If, in the future, Nvim itself is used to run the test-suite -- instead of "vanilla Lua", these functions could move to src/nvim/lua/vim.lua -local vim = {} +local vim = vim or {} --- Returns a deep copy of the given object. Non-table objects are copied as --- in a typical Lua assignment, whereas table objects are copied recursively. -- cgit From b35f6aa9dd686d082405132e3455cd0aff334361 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Sun, 24 Nov 2019 03:01:18 -0800 Subject: Add support for textDocument/references. Add set_qflist and set_loclist. - Also add locations_to_items, which calculates byte offsets for character positions in files and avoids unnecessary operations. --- runtime/lua/vim/lsp/buf.lua | 15 ++++++ runtime/lua/vim/lsp/default_callbacks.lua | 2 +- runtime/lua/vim/lsp/util.lua | 80 +++++++++++++++++++++++-------- 3 files changed, 77 insertions(+), 20 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 79d43fda4a..51295e5570 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -1,3 +1,4 @@ +local vim = vim local validate = vim.validate local api = vim.api local vfn = vim.fn @@ -330,5 +331,19 @@ function M.rename(new_name) end) end +function M.references(context) + validate { context = { context, 't', true } } + local params = util.make_position_params() + params.context = context or { + includeDeclaration = true; + } + params[vim.type_idx] = vim.types.dictionary + request('textDocument/references', params, function(_, _, result) + if not result then return end + util.set_qflist(result) + vim.api.nvim_command("copen") + end) +end + return M -- vim:sw=2 ts=2 et diff --git a/runtime/lua/vim/lsp/default_callbacks.lua b/runtime/lua/vim/lsp/default_callbacks.lua index d58280e0f2..2a891e7d1d 100644 --- a/runtime/lua/vim/lsp/default_callbacks.lua +++ b/runtime/lua/vim/lsp/default_callbacks.lua @@ -31,7 +31,7 @@ M['textDocument/publishDiagnostics'] = function(_, _, result) util.buf_diagnostics_save_positions(bufnr, result.diagnostics) util.buf_diagnostics_underline(bufnr, result.diagnostics) util.buf_diagnostics_virtual_text(bufnr, result.diagnostics) - -- util.buf_loclist(bufnr, result.diagnostics) + -- util.set_loclist(result.diagnostics) end local function log_message(_, _, result, client_id) diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 0c53494f02..2dfcdfc70c 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -1,4 +1,5 @@ local protocol = require 'vim.lsp.protocol' +local vim = vim local validate = vim.validate local api = vim.api @@ -573,30 +574,71 @@ do end end -function M.buf_loclist(bufnr, locations) - local targetwin - for _, winnr in ipairs(api.nvim_list_wins()) do - local winbuf = api.nvim_win_get_buf(winnr) - if winbuf == bufnr then - targetwin = winnr - break - end - end - if not targetwin then return end +local position_sort = sort_by_key(function(v) + return {v.line, v.character} +end) +-- Returns the items with the byte position calculated correctly and in sorted +-- order. +function M.locations_to_items(locations) local items = {} - local path = api.nvim_buf_get_name(bufnr) + local grouped = setmetatable({}, { + __index = function(t, k) + local v = {} + rawset(t, k, v) + return v + end; + }) for _, d in ipairs(locations) do - -- TODO: URL parsing here? local start = d.range.start - table.insert(items, { - filename = path, - lnum = start.line + 1, - col = start.character + 1, - text = d.message, - }) + local fname = assert(vim.uri_to_fname(d.uri)) + table.insert(grouped[fname], start) + end + local keys = vim.tbl_keys(grouped) + table.sort(keys) + -- TODO(ashkan) I wish we could do this lazily. + for _, fname in ipairs(keys) do + local rows = grouped[fname] + table.sort(rows, position_sort) + local i = 0 + for line in io.lines(fname) do + for _, pos in ipairs(rows) do + local row = pos.line + if i == row then + local col + if pos.character > #line then + col = #line + else + col = vim.str_byteindex(line, pos.character) + end + table.insert(items, { + filename = fname, + lnum = row + 1, + col = col + 1; + }) + end + end + i = i + 1 + end end - vim.fn.setloclist(targetwin, items, ' ', 'Language Server') + return items +end + +-- locations is Location[] +-- Only sets for the current window. +function M.set_loclist(locations) + vim.fn.setloclist(0, {}, ' ', { + title = 'Language Server'; + items = M.locations_to_items(locations); + }) +end + +-- locations is Location[] +function M.set_qflist(locations) + vim.fn.setqflist({}, ' ', { + title = 'Language Server'; + items = M.locations_to_items(locations); + }) end -- Remove empty lines from the beginning and end. -- cgit From a9036502dca7191c8ada70367e44c398d48dd94a Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Sun, 24 Nov 2019 03:14:03 -0800 Subject: Bring vim into local scope --- runtime/lua/vim/lsp.lua | 1 + 1 file changed, 1 insertion(+) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 7076db68d3..928d19177a 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -4,6 +4,7 @@ local lsp_rpc = require 'vim.lsp.rpc' local protocol = require 'vim.lsp.protocol' local util = require 'vim.lsp.util' +local vim = vim local nvim_err_writeln, nvim_buf_get_lines, nvim_command, nvim_buf_get_option = vim.api.nvim_err_writeln, vim.api.nvim_buf_get_lines, vim.api.nvim_command, vim.api.nvim_buf_get_option local uv = vim.loop -- cgit From fd5710ae9a3bcbc0f9cbb71de9e39253350ff09c Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Mon, 25 Nov 2019 01:08:02 -0800 Subject: doc + extmarks tweaks #11421 - nvim_buf_get_extmarks: rename "amount" => "limit" - rename `set_extmark_index_from_obj` --- runtime/lua/vim/shared.lua | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 1912d3708d..631dd04c35 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -226,16 +226,17 @@ function vim.tbl_add_reverse_lookup(o) return o end --- Extends a list-like table with the values of another list-like table. --- --- NOTE: This *mutates* dst! --- @see |extend()| --- --- @param dst list which will be modified and appended to. --- @param src list from which values will be inserted. --- @param start Start index on src. defaults to 1 --- @param finish Final index on src. defaults to #src --- @returns dst +--- Extends a list-like table with the values of another list-like table. +--- +--- NOTE: This mutates dst! +--- +--@see |vim.tbl_extend()| +--- +--@param dst list which will be modified and appended to. +--@param src list from which values will be inserted. +--@param start Start index on src. defaults to 1 +--@param finish Final index on src. defaults to #src +--@returns dst function vim.list_extend(dst, src, start, finish) vim.validate { dst = {dst, 't'}; -- cgit From 6e8c5779cf960893850501e4871dc9be671db298 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Tue, 26 Nov 2019 05:59:40 -0800 Subject: LSP: Move default buf callbacks to vim.lsp.callbacks (#11452) - In the process, refactored focusable_preview to a util function. - Add text for locations_to_items of the current line. - Improve location callback to handle multiple return values by using set_qflist. - Remove update_tagstack and leave note for future travelers. --- runtime/lua/vim/lsp.lua | 6 +- runtime/lua/vim/lsp/buf.lua | 253 +++--------------------------- runtime/lua/vim/lsp/callbacks.lua | 223 ++++++++++++++++++++++++++ runtime/lua/vim/lsp/default_callbacks.lua | 69 -------- runtime/lua/vim/lsp/util.lua | 56 ++++++- 5 files changed, 300 insertions(+), 307 deletions(-) create mode 100644 runtime/lua/vim/lsp/callbacks.lua delete mode 100644 runtime/lua/vim/lsp/default_callbacks.lua (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 928d19177a..82f4fda66f 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -1,4 +1,4 @@ -local default_callbacks = require 'vim.lsp.default_callbacks' +local default_callbacks = require 'vim.lsp.callbacks' local log = require 'vim.lsp.log' local lsp_rpc = require 'vim.lsp.rpc' local protocol = require 'vim.lsp.protocol' @@ -13,7 +13,7 @@ local validate = vim.validate local lsp = { protocol = protocol; - default_callbacks = default_callbacks; + callbacks = default_callbacks; buf = require'vim.lsp.buf'; util = util; -- Allow raw RPC access. @@ -469,7 +469,7 @@ function lsp.start_client(config) function client.request(method, params, callback) if not callback then callback = resolve_callback(method) - or error(string.format("request callback is empty and no default was found for client %s", client.name)) + or error("not found: request callback for client "..client.name) end local _ = log.debug() and log.debug(log_prefix, "client.request", client_id, method, params, callback) -- TODO keep these checks or just let it go anyway? diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 51295e5570..a6a05fb095 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -3,7 +3,6 @@ local validate = vim.validate local api = vim.api local vfn = vim.fn local util = require 'vim.lsp.util' -local log = require 'vim.lsp.log' local list_extend = vim.list_extend local M = {} @@ -16,270 +15,68 @@ local function npcall(fn, ...) return ok_or_nil(pcall(fn, ...)) end -local function err_message(...) - api.nvim_err_writeln(table.concat(vim.tbl_flatten{...})) - api.nvim_command("redraw") -end - -local function find_window_by_var(name, value) - for _, win in ipairs(api.nvim_list_wins()) do - if npcall(api.nvim_win_get_var, win, name) == value then - return win - end - end -end - local function request(method, params, callback) - -- TODO(ashkan) enable this. - -- callback = vim.lsp.default_callbacks[method] or callback validate { method = {method, 's'}; - callback = {callback, 'f'}; + callback = {callback, 'f', true}; } - return vim.lsp.buf_request(0, method, params, function(err, _, result, client_id) - local _ = log.debug() and log.debug("vim.lsp.buf", method, client_id, err, result) - if err then error(tostring(err)) end - return callback(err, method, result, client_id) - end) -end - -local function focusable_preview(method, params, fn) - if npcall(api.nvim_win_get_var, 0, method) then - return api.nvim_command("wincmd p") - end - - local bufnr = api.nvim_get_current_buf() - do - local win = find_window_by_var(method, bufnr) - if win then - api.nvim_set_current_win(win) - api.nvim_command("stopinsert") - return - end - end - return request(method, params, function(_, _, result, _) - -- TODO(ashkan) could show error in preview... - local lines, filetype, opts = fn(result) - if lines then - local _, winnr = util.open_floating_preview(lines, filetype, opts) - api.nvim_win_set_var(winnr, method, bufnr) - end - end) + return vim.lsp.buf_request(0, method, params, callback) end function M.hover() local params = util.make_position_params() - focusable_preview('textDocument/hover', params, function(result) - if not (result and result.contents) then return end - - local markdown_lines = util.convert_input_to_markdown_lines(result.contents) - markdown_lines = util.trim_empty_lines(markdown_lines) - if vim.tbl_isempty(markdown_lines) then - return { 'No information available' } - end - return markdown_lines, util.try_trim_markdown_code_blocks(markdown_lines) - end) + request('textDocument/hover', params) end function M.peek_definition() local params = util.make_position_params() - request('textDocument/peekDefinition', params, function(_, _, result, _) - if not (result and result[1]) then return end - local loc = result[1] - local bufnr = vim.uri_to_bufnr(loc.uri) or error("couldn't find file "..tostring(loc.uri)) - local start = loc.range.start - local finish = loc.range["end"] - util.open_floating_peek_preview(bufnr, start, finish, { offset_x = 1 }) - local headbuf = util.open_floating_preview({"Peek:"}, nil, { - offset_y = -(finish.line - start.line); - width = finish.character - start.character + 2; - }) - -- TODO(ashkan) change highlight group? - api.nvim_buf_add_highlight(headbuf, -1, 'Keyword', 0, -1) - end) + request('textDocument/peekDefinition', params) end -local function update_tagstack() - local bufnr = api.nvim_get_current_buf() - local line = vfn.line('.') - local col = vfn.col('.') - local tagname = vfn.expand('') - local item = { bufnr = bufnr, from = { bufnr, line, col, 0 }, tagname = tagname } - local winid = vfn.win_getid() - local tagstack = vfn.gettagstack(winid) - local action - if tagstack.length == tagstack.curidx then - action = 'r' - tagstack.items[tagstack.curidx] = item - elseif tagstack.length > tagstack.curidx then - action = 'r' - if tagstack.curidx > 1 then - tagstack.items = table.insert(tagstack.items[tagstack.curidx - 1], item) - else - tagstack.items = { item } - end - else - action = 'a' - tagstack.items = { item } - end - tagstack.curidx = tagstack.curidx + 1 - vfn.settagstack(winid, tagstack, action) -end -local function handle_location(result) - -- We can sometimes get a list of locations, so set the first value as the - -- only value we want to handle - -- TODO(ashkan) was this correct^? We could use location lists. - if result[1] ~= nil then - result = result[1] - end - if result.uri == nil then - err_message('[LSP] Could not find a valid location') - return - end - local bufnr = vim.uri_to_bufnr(result.uri) - update_tagstack() - api.nvim_set_current_buf(bufnr) - local row = result.range.start.line - local col = result.range.start.character - local line = api.nvim_buf_get_lines(0, row, row+1, true)[1] - col = #line:sub(1, col) - api.nvim_win_set_cursor(0, {row + 1, col}) - return true -end -local function location_callback(_, method, result) - if result == nil or vim.tbl_isempty(result) then - local _ = log.info() and log.info(method, 'No location found') - return nil - end - return handle_location(result) -end - function M.declaration() local params = util.make_position_params() - request('textDocument/declaration', params, location_callback) + request('textDocument/declaration', params) end function M.definition() local params = util.make_position_params() - request('textDocument/definition', params, location_callback) + request('textDocument/definition', params) end function M.type_definition() local params = util.make_position_params() - request('textDocument/typeDefinition', params, location_callback) + request('textDocument/typeDefinition', params) end function M.implementation() local params = util.make_position_params() - request('textDocument/implementation', params, location_callback) -end - ---- Convert SignatureHelp response to preview contents. --- https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#textDocument_signatureHelp -local function signature_help_to_preview_contents(input) - if not input.signatures then - return - end - --The active signature. If omitted or the value lies outside the range of - --`signatures` the value defaults to zero or is ignored if `signatures.length - --=== 0`. Whenever possible implementors should make an active decision about - --the active signature and shouldn't rely on a default value. - local contents = {} - local active_signature = input.activeSignature or 0 - -- If the activeSignature is not inside the valid range, then clip it. - if active_signature >= #input.signatures then - active_signature = 0 - end - local signature = input.signatures[active_signature + 1] - if not signature then - return - end - list_extend(contents, vim.split(signature.label, '\n', true)) - if signature.documentation then - util.convert_input_to_markdown_lines(signature.documentation, contents) - end - if input.parameters then - local active_parameter = input.activeParameter or 0 - -- If the activeParameter is not inside the valid range, then clip it. - if active_parameter >= #input.parameters then - active_parameter = 0 - end - local parameter = signature.parameters and signature.parameters[active_parameter] - if parameter then - --[=[ - --Represents a parameter of a callable-signature. A parameter can - --have a label and a doc-comment. - interface ParameterInformation { - --The label of this parameter information. - -- - --Either a string or an inclusive start and exclusive end offsets within its containing - --signature label. (see SignatureInformation.label). The offsets are based on a UTF-16 - --string representation as `Position` and `Range` does. - -- - --*Note*: a label of type string should be a substring of its containing signature label. - --Its intended use case is to highlight the parameter label part in the `SignatureInformation.label`. - label: string | [number, number]; - --The human-readable doc-comment of this parameter. Will be shown - --in the UI but can be omitted. - documentation?: string | MarkupContent; - } - --]=] - -- TODO highlight parameter - if parameter.documentation then - util.convert_input_to_markdown_lines(parameter.documentation, contents) - end - end - end - return contents + request('textDocument/implementation', params) end function M.signature_help() local params = util.make_position_params() - focusable_preview('textDocument/signatureHelp', params, function(result) - if not (result and result.signatures and result.signatures[1]) then - return { 'No signature available' } - end - - -- TODO show empty popup when signatures is empty? - local lines = signature_help_to_preview_contents(result) - lines = util.trim_empty_lines(lines) - if vim.tbl_isempty(lines) then - return { 'No signature available' } - end - return lines, util.try_trim_markdown_code_blocks(lines) - end) + request('textDocument/signatureHelp', params) end -- TODO(ashkan) ? function M.completion(context) local params = util.make_position_params() params.context = context - return request('textDocument/completion', params, function(_, _, result) - if vim.tbl_isempty(result or {}) then return end - local row, col = unpack(api.nvim_win_get_cursor(0)) - local line = assert(api.nvim_buf_get_lines(0, row-1, row, false)[1]) - local line_to_cursor = line:sub(col+1) - - local matches = util.text_document_completion_list_to_complete_items(result, line_to_cursor) - vim.fn.complete(col, matches) - end) + return request('textDocument/completion', params) end function M.formatting(options) validate { options = {options, 't', true} } options = vim.tbl_extend('keep', options or {}, { - tabSize = api.nvim_buf_get_option(0, 'tabstop'); - insertSpaces = api.nvim_buf_get_option(0, 'expandtab'); + tabSize = vim.bo.tabstop; + insertSpaces = vim.bo.expandtab; }) local params = { textDocument = { uri = vim.uri_from_bufnr(0) }; options = options; } - return request('textDocument/formatting', params, function(_, _, result) - if not result then return end - util.apply_text_edits(result) - end) + return request('textDocument/formatting', params) end function M.range_formatting(options, start_pos, end_pos) @@ -289,8 +86,8 @@ function M.range_formatting(options, start_pos, end_pos) end_pos = {end_pos, 't', true}; } options = vim.tbl_extend('keep', options or {}, { - tabSize = api.nvim_buf_get_option(0, 'tabstop'); - insertSpaces = api.nvim_buf_get_option(0, 'expandtab'); + tabSize = vim.bo.tabstop; + insertSpaces = vim.bo.expandtab; }) local A = list_extend({}, start_pos or api.nvim_buf_get_mark(0, '<')) local B = list_extend({}, end_pos or api.nvim_buf_get_mark(0, '>')) @@ -299,10 +96,10 @@ function M.range_formatting(options, start_pos, end_pos) B[1] = B[1] - 1 -- account for encoding. if A[2] > 0 then - A = {A[1], util.character_offset(0, unpack(A))} + A = {A[1], util.character_offset(0, A[1], A[2])} end if B[2] > 0 then - B = {B[1], util.character_offset(0, unpack(B))} + B = {B[1], util.character_offset(0, B[1], B[2])} end local params = { textDocument = { uri = vim.uri_from_bufnr(0) }; @@ -312,10 +109,7 @@ function M.range_formatting(options, start_pos, end_pos) }; options = options; } - return request('textDocument/rangeFormatting', params, function(_, _, result) - if not result then return end - util.apply_text_edits(result) - end) + return request('textDocument/rangeFormatting', params) end function M.rename(new_name) @@ -325,10 +119,7 @@ function M.rename(new_name) new_name = new_name or npcall(vfn.input, "New Name: ") if not (new_name and #new_name > 0) then return end params.newName = new_name - request('textDocument/rename', params, function(_, _, result) - if not result then return end - util.apply_workspace_edit(result) - end) + request('textDocument/rename', params) end function M.references(context) @@ -338,11 +129,7 @@ function M.references(context) includeDeclaration = true; } params[vim.type_idx] = vim.types.dictionary - request('textDocument/references', params, function(_, _, result) - if not result then return end - util.set_qflist(result) - vim.api.nvim_command("copen") - end) + request('textDocument/references', params) end return M diff --git a/runtime/lua/vim/lsp/callbacks.lua b/runtime/lua/vim/lsp/callbacks.lua new file mode 100644 index 0000000000..4fc3f74519 --- /dev/null +++ b/runtime/lua/vim/lsp/callbacks.lua @@ -0,0 +1,223 @@ +local log = require 'vim.lsp.log' +local protocol = require 'vim.lsp.protocol' +local util = require 'vim.lsp.util' +local vim = vim +local api = vim.api + +local M = {} + +local function err_message(...) + api.nvim_err_writeln(table.concat(vim.tbl_flatten{...})) + api.nvim_command("redraw") +end + +M['workspace/applyEdit'] = function(_, _, workspace_edit) + if not workspace_edit then return end + -- TODO(ashkan) Do something more with label? + if workspace_edit.label then + print("Workspace edit", workspace_edit.label) + end + util.apply_workspace_edit(workspace_edit.edit) +end + +M['textDocument/publishDiagnostics'] = function(_, _, result) + if not result then return end + local uri = result.uri + local bufnr = vim.uri_to_bufnr(uri) + if not bufnr then + err_message("LSP.publishDiagnostics: Couldn't find buffer for ", uri) + return + end + util.buf_clear_diagnostics(bufnr) + util.buf_diagnostics_save_positions(bufnr, result.diagnostics) + util.buf_diagnostics_underline(bufnr, result.diagnostics) + util.buf_diagnostics_virtual_text(bufnr, result.diagnostics) + -- util.set_loclist(result.diagnostics) +end + +M['textDocument/references'] = function(_, _, result) + if not result then return end + util.set_qflist(result) + api.nvim_command("copen") + api.nvim_command("wincmd p") +end + +M['textDocument/rename'] = function(_, _, result) + if not result then return end + util.apply_workspace_edit(result) +end + +M['textDocument/rangeFormatting'] = function(_, _, result) + if not result then return end + util.apply_text_edits(result) +end + +M['textDocument/formatting'] = function(_, _, result) + if not result then return end + util.apply_text_edits(result) +end + +M['textDocument/completion'] = function(_, _, result) + if vim.tbl_isempty(result or {}) then return end + local row, col = unpack(api.nvim_win_get_cursor(0)) + local line = assert(api.nvim_buf_get_lines(0, row-1, row, false)[1]) + local line_to_cursor = line:sub(col+1) + + local matches = util.text_document_completion_list_to_complete_items(result, line_to_cursor) + vim.fn.complete(col, matches) +end + +M['textDocument/hover'] = function(_, method, result) + util.focusable_preview(method, function() + if not (result and result.contents) then + return { 'No information available' } + end + local markdown_lines = util.convert_input_to_markdown_lines(result.contents) + markdown_lines = util.trim_empty_lines(markdown_lines) + if vim.tbl_isempty(markdown_lines) then + return { 'No information available' } + end + return markdown_lines, util.try_trim_markdown_code_blocks(markdown_lines) + end) +end + +local function location_callback(_, method, result) + if result == nil or vim.tbl_isempty(result) then + local _ = log.info() and log.info(method, 'No location found') + return nil + end + util.jump_to_location(result[1]) + if #result > 1 then + util.set_qflist(result) + api.nvim_command("copen") + api.nvim_command("wincmd p") + end +end + +M['textDocument/declaration'] = location_callback +M['textDocument/definition'] = location_callback +M['textDocument/typeDefinition'] = location_callback +M['textDocument/implementation'] = location_callback + +--- Convert SignatureHelp response to preview contents. +-- https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#textDocument_signatureHelp +local function signature_help_to_preview_contents(input) + if not input.signatures then + return + end + --The active signature. If omitted or the value lies outside the range of + --`signatures` the value defaults to zero or is ignored if `signatures.length + --=== 0`. Whenever possible implementors should make an active decision about + --the active signature and shouldn't rely on a default value. + local contents = {} + local active_signature = input.activeSignature or 0 + -- If the activeSignature is not inside the valid range, then clip it. + if active_signature >= #input.signatures then + active_signature = 0 + end + local signature = input.signatures[active_signature + 1] + if not signature then + return + end + vim.list_extend(contents, vim.split(signature.label, '\n', true)) + if signature.documentation then + util.convert_input_to_markdown_lines(signature.documentation, contents) + end + if input.parameters then + local active_parameter = input.activeParameter or 0 + -- If the activeParameter is not inside the valid range, then clip it. + if active_parameter >= #input.parameters then + active_parameter = 0 + end + local parameter = signature.parameters and signature.parameters[active_parameter] + if parameter then + --[=[ + --Represents a parameter of a callable-signature. A parameter can + --have a label and a doc-comment. + interface ParameterInformation { + --The label of this parameter information. + -- + --Either a string or an inclusive start and exclusive end offsets within its containing + --signature label. (see SignatureInformation.label). The offsets are based on a UTF-16 + --string representation as `Position` and `Range` does. + -- + --*Note*: a label of type string should be a substring of its containing signature label. + --Its intended use case is to highlight the parameter label part in the `SignatureInformation.label`. + label: string | [number, number]; + --The human-readable doc-comment of this parameter. Will be shown + --in the UI but can be omitted. + documentation?: string | MarkupContent; + } + --]=] + -- TODO highlight parameter + if parameter.documentation then + util.convert_input_to_markdown_lines(parameter.documentation, contents) + end + end + end + return contents +end + +M['textDocument/signatureHelp'] = function(_, method, result) + util.focusable_preview(method, function() + if not (result and result.signatures and result.signatures[1]) then + return { 'No signature available' } + end + -- TODO show popup when signatures is empty? + local lines = signature_help_to_preview_contents(result) + lines = util.trim_empty_lines(lines) + if vim.tbl_isempty(lines) then + return { 'No signature available' } + end + return lines, util.try_trim_markdown_code_blocks(lines) + end) +end + +M['textDocument/peekDefinition'] = function(_, _, result, _) + if not (result and result[1]) then return end + local loc = result[1] + local bufnr = vim.uri_to_bufnr(loc.uri) or error("not found: "..tostring(loc.uri)) + local start = loc.range.start + local finish = loc.range["end"] + util.open_floating_peek_preview(bufnr, start, finish, { offset_x = 1 }) + local headbuf = util.open_floating_preview({"Peek:"}, nil, { + offset_y = -(finish.line - start.line); + width = finish.character - start.character + 2; + }) + -- TODO(ashkan) change highlight group? + api.nvim_buf_add_highlight(headbuf, -1, 'Keyword', 0, -1) +end + +local function log_message(_, _, result, client_id) + local message_type = result.type + local message = result.message + local client = vim.lsp.get_client_by_id(client_id) + local client_name = client and client.name or string.format("id=%d", client_id) + if not client then + err_message("LSP[", client_name, "] client has shut down after sending the message") + end + if message_type == protocol.MessageType.Error then + err_message("LSP[", client_name, "] ", message) + else + local message_type_name = protocol.MessageType[message_type] + api.nvim_out_write(string.format("LSP[%s][%s] %s\n", client_name, message_type_name, message)) + end + return result +end + +M['window/showMessage'] = log_message +M['window/logMessage'] = log_message + +-- Add boilerplate error validation and logging for all of these. +for k, fn in pairs(M) do + M[k] = function(err, method, params, client_id) + local _ = log.debug() and log.debug('default_callback', method, { params = params, client_id = client_id, err = err }) + if err then + error(tostring(err)) + end + return fn(err, method, params, client_id) + end +end + +return M +-- vim:sw=2 ts=2 et diff --git a/runtime/lua/vim/lsp/default_callbacks.lua b/runtime/lua/vim/lsp/default_callbacks.lua deleted file mode 100644 index 2a891e7d1d..0000000000 --- a/runtime/lua/vim/lsp/default_callbacks.lua +++ /dev/null @@ -1,69 +0,0 @@ -local log = require 'vim.lsp.log' -local protocol = require 'vim.lsp.protocol' -local util = require 'vim.lsp.util' -local api = vim.api - -local M = {} - -local function err_message(...) - api.nvim_err_writeln(table.concat(vim.tbl_flatten{...})) - api.nvim_command("redraw") -end - -M['workspace/applyEdit'] = function(_, _, workspace_edit) - if not workspace_edit then return end - -- TODO(ashkan) Do something more with label? - if workspace_edit.label then - print("Workspace edit", workspace_edit.label) - end - util.apply_workspace_edit(workspace_edit.edit) -end - -M['textDocument/publishDiagnostics'] = function(_, _, result) - if not result then return end - local uri = result.uri - local bufnr = vim.uri_to_bufnr(uri) - if not bufnr then - err_message("LSP.publishDiagnostics: Couldn't find buffer for ", uri) - return - end - util.buf_clear_diagnostics(bufnr) - util.buf_diagnostics_save_positions(bufnr, result.diagnostics) - util.buf_diagnostics_underline(bufnr, result.diagnostics) - util.buf_diagnostics_virtual_text(bufnr, result.diagnostics) - -- util.set_loclist(result.diagnostics) -end - -local function log_message(_, _, result, client_id) - local message_type = result.type - local message = result.message - local client = vim.lsp.get_client_by_id(client_id) - local client_name = client and client.name or string.format("id=%d", client_id) - if not client then - err_message("LSP[", client_name, "] client has shut down after sending the message") - end - if message_type == protocol.MessageType.Error then - err_message("LSP[", client_name, "] ", message) - else - local message_type_name = protocol.MessageType[message_type] - api.nvim_out_write(string.format("LSP[%s][%s] %s\n", client_name, message_type_name, message)) - end - return result -end - -M['window/showMessage'] = log_message -M['window/logMessage'] = log_message - --- Add boilerplate error validation and logging for all of these. -for k, fn in pairs(M) do - M[k] = function(err, method, params, client_id) - local _ = log.debug() and log.debug('default_callback', method, { params = params, client_id = client_id, err = err }) - if err then - error(tostring(err)) - end - return fn(err, method, params, client_id) - end -end - -return M --- vim:sw=2 ts=2 et diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 2dfcdfc70c..6e0d3fd4ee 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -2,6 +2,7 @@ local protocol = require 'vim.lsp.protocol' local vim = vim local validate = vim.validate local api = vim.api +local list_extend = vim.list_extend local M = {} @@ -10,7 +11,13 @@ local function split_lines(value) return split(value, '\n', true) end -local list_extend = vim.list_extend +local function ok_or_nil(status, ...) + if not status then return end + return ... +end +local function npcall(fn, ...) + return ok_or_nil(pcall(fn, ...)) +end --- Find the longest shared prefix between prefix and word. -- e.g. remove_prefix("123tes", "testing") == "ting" @@ -303,6 +310,50 @@ function M.make_floating_popup_options(width, height, opts) } end +function M.jump_to_location(location) + if location.uri == nil then return end + local bufnr = vim.uri_to_bufnr(location.uri) + -- TODO(ashkan) use tagfunc here to update tagstack. + api.nvim_set_current_buf(bufnr) + local row = location.range.start.line + local col = location.range.start.character + local line = api.nvim_buf_get_lines(0, row, row+1, true)[1] + col = vim.str_byteindex(line, col) + api.nvim_win_set_cursor(0, {row + 1, col}) + return true +end + +local function find_window_by_var(name, value) + for _, win in ipairs(api.nvim_list_wins()) do + if npcall(api.nvim_win_get_var, win, name) == value then + return win + end + end +end + +-- Check if a window with `unique_name` tagged is associated with the current +-- buffer. If not, make a new preview. +-- +-- fn()'s return values will be passed directly to open_floating_preview in the +-- case that a new floating window should be created. +function M.focusable_preview(unique_name, fn) + if npcall(api.nvim_win_get_var, 0, unique_name) then + return api.nvim_command("wincmd p") + end + local bufnr = api.nvim_get_current_buf() + do + local win = find_window_by_var(unique_name, bufnr) + if win then + api.nvim_set_current_win(win) + api.nvim_command("stopinsert") + return + end + end + local pbufnr, pwinnr = M.open_floating_preview(fn()) + api.nvim_win_set_var(pwinnr, unique_name, bufnr) + return pbufnr, pwinnr +end + function M.open_floating_preview(contents, filetype, opts) validate { contents = { contents, 't' }; @@ -609,12 +660,13 @@ function M.locations_to_items(locations) if pos.character > #line then col = #line else - col = vim.str_byteindex(line, pos.character) + col = vim.str_byteindex(line, pos.character) end table.insert(items, { filename = fname, lnum = row + 1, col = col + 1; + text = line; }) end end -- cgit From 950690e48a6ff4788b3ad6c23caa99d253facd57 Mon Sep 17 00:00:00 2001 From: Björn Linse Date: Wed, 27 Nov 2019 19:45:03 +0100 Subject: lsp: allow the user to config LspDiagnosticError etc by standard means --- runtime/lua/vim/lsp/util.lua | 71 ++++++++++---------------------------------- 1 file changed, 15 insertions(+), 56 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 6e0d3fd4ee..b9990ed082 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -440,6 +440,11 @@ do local diagnostic_ns = api.nvim_create_namespace("vim_lsp_diagnostics") + local underline_highlight_name = "LspDiagnosticsUnderline" + api.nvim_command(string.format("highlight default %s gui=underline cterm=underline", underline_highlight_name)) + + local severity_highlights = {} + local default_severity_highlight = { [protocol.DiagnosticSeverity.Error] = { guifg = "Red" }; [protocol.DiagnosticSeverity.Warning] = { guifg = "Orange" }; @@ -447,60 +452,17 @@ do [protocol.DiagnosticSeverity.Hint] = { guifg = "LightGrey" }; } - local underline_highlight_name = "LspDiagnosticsUnderline" - api.nvim_command(string.format("highlight %s gui=underline cterm=underline", underline_highlight_name)) - - local function find_color_rgb(color) - local rgb_hex = api.nvim_get_color_by_name(color) - validate { color = {color, function() return rgb_hex ~= -1 end, "valid color name"} } - return rgb_hex - end - - --- Determine whether to use black or white text - -- Ref: https://stackoverflow.com/a/1855903/837964 - -- https://stackoverflow.com/questions/596216/formula-to-determine-brightness-of-rgb-color - local function color_is_bright(r, g, b) - -- Counting the perceptive luminance - human eye favors green color - local luminance = (0.299*r + 0.587*g + 0.114*b)/255 - if luminance > 0.5 then - return true -- Bright colors, black font - else - return false -- Dark colors, white font - end - end - - local severity_highlights = {} - - function M.set_severity_highlights(highlights) - validate {highlights = {highlights, 't'}} - for severity, default_color in pairs(default_severity_highlight) do - local severity_name = protocol.DiagnosticSeverity[severity] - local highlight_name = "LspDiagnostics"..severity_name - local hi_info = highlights[severity] or default_color - -- Try to fill in the foreground color with a sane default. - if not hi_info.guifg and hi_info.guibg then - -- TODO(ashkan) move this out when bitop is guaranteed to be included. - local bit = require 'bit' - local band, rshift = bit.band, bit.rshift - local rgb = find_color_rgb(hi_info.guibg) - local is_bright = color_is_bright(rshift(rgb, 16), band(rshift(rgb, 8), 0xFF), band(rgb, 0xFF)) - hi_info.guifg = is_bright and "Black" or "White" - end - if not hi_info.ctermfg and hi_info.ctermbg then - -- TODO(ashkan) move this out when bitop is guaranteed to be included. - local bit = require 'bit' - local band, rshift = bit.band, bit.rshift - local rgb = find_color_rgb(hi_info.ctermbg) - local is_bright = color_is_bright(rshift(rgb, 16), band(rshift(rgb, 8), 0xFF), band(rgb, 0xFF)) - hi_info.ctermfg = is_bright and "Black" or "White" - end - local cmd_parts = {"highlight", highlight_name} - for k, v in pairs(hi_info) do - table.insert(cmd_parts, k.."="..v) - end - api.nvim_command(table.concat(cmd_parts, ' ')) - severity_highlights[severity] = highlight_name + -- Initialize default severity highlights + for severity, hi_info in pairs(default_severity_highlight) do + local severity_name = protocol.DiagnosticSeverity[severity] + local highlight_name = "LspDiagnostics"..severity_name + -- Try to fill in the foreground color with a sane default. + local cmd_parts = {"highlight", "default", highlight_name} + for k, v in pairs(hi_info) do + table.insert(cmd_parts, k.."="..v) end + api.nvim_command(table.concat(cmd_parts, ' ')) + severity_highlights[severity] = highlight_name end function M.buf_clear_diagnostics(bufnr) @@ -509,9 +471,6 @@ do api.nvim_buf_clear_namespace(bufnr, diagnostic_ns, 0, -1) end - -- Initialize with the defaults. - M.set_severity_highlights(default_severity_highlight) - function M.get_severity_highlight_name(severity) return severity_highlights[severity] end -- cgit From 70b606166640d043fc7b78a52b89ff1bba798b6a Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Sun, 1 Dec 2019 05:32:55 -0800 Subject: Add vim.startswith and vim.endswith (#11248) --- runtime/lua/vim/shared.lua | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 631dd04c35..b5b04d7757 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -320,6 +320,26 @@ function vim.pesc(s) return s:gsub('[%(%)%.%%%+%-%*%?%[%]%^%$]', '%%%1') end +--- Test if `prefix` is a prefix of `s` for strings. +-- +-- @param s String to check +-- @param prefix Potential prefix +-- @return boolean True if prefix is a prefix of s +function vim.startswith(s, prefix) + vim.validate { s = {s, 's'}; prefix = {prefix, 's'}; } + return s:sub(1, #prefix) == prefix +end + +--- Test if `suffix` is a suffix of `s` for strings. +-- +-- @param s String to check +-- @param suffix Potential suffix +-- @return boolean True if suffix is a suffix of s +function vim.endswith(s, suffix) + vim.validate { s = {s, 's'}; suffix = {suffix, 's'}; } + return #suffix == 0 or s:sub(-#suffix) == suffix +end + --- Validates a parameter specification (types and values). --- --- Usage example: -- cgit From 30ed245d008045a505a87ab21d22ada45dd1f4b5 Mon Sep 17 00:00:00 2001 From: Jakub Łuczyński Date: Sat, 7 Dec 2019 12:34:22 +0100 Subject: LSP: Add jump when calling gotodef (#11521) --- runtime/lua/vim/lsp/util.lua | 2 ++ 1 file changed, 2 insertions(+) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index b9990ed082..3798b30a46 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -313,6 +313,8 @@ end function M.jump_to_location(location) if location.uri == nil then return end local bufnr = vim.uri_to_bufnr(location.uri) + -- Save position in jumplist + vim.cmd "normal! m'" -- TODO(ashkan) use tagfunc here to update tagstack. api.nvim_set_current_buf(bufnr) local row = location.range.start.line -- cgit From d00c624ba4731ba7bfe9bd71e2cc32e54f886342 Mon Sep 17 00:00:00 2001 From: Mike Hartington Date: Fri, 20 Dec 2019 05:46:47 -0500 Subject: LSP: fix omnifunc findstart (#11522) --- runtime/lua/vim/lsp.lua | 37 ++++++++++++++++--------------------- runtime/lua/vim/lsp/util.lua | 25 ++----------------------- 2 files changed, 18 insertions(+), 44 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 82f4fda66f..501cc6f670 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -859,29 +859,21 @@ function lsp.omnifunc(findstart, base) end if findstart == 1 then + -- First, just return the current cursor column, we only really need that return vim.fn.col('.') else + -- Then, perform standard completion request + log.info("base ", base) + local pos = vim.api.nvim_win_get_cursor(0) - local line = assert(nvim_buf_get_lines(bufnr, pos[1]-1, pos[1], false)[1]) + local line = vim.api.nvim_get_current_line() + local line_to_cursor = line:sub(1, pos[2]) local _ = log.trace() and log.trace("omnifunc.line", pos, line) - local line_to_cursor = line:sub(1, pos[2]+1) - local _ = log.trace() and log.trace("omnifunc.line_to_cursor", line_to_cursor) - local params = { - textDocument = { - uri = vim.uri_from_bufnr(bufnr); - }; - position = { - -- 0-indexed for both line and character - line = pos[1] - 1, - character = vim.str_utfindex(line, pos[2]), - }; - -- The completion context. This is only available if the client specifies - -- to send this using `ClientCapabilities.textDocument.completion.contextSupport === true` - -- context = nil or { - -- triggerKind = protocol.CompletionTriggerKind.Invoked; - -- triggerCharacter = nil or ""; - -- }; - } + + -- Get the start postion of the current keyword + local textMatch = vim.fn.match(line_to_cursor, '\\k*$') + local params = util.make_position_params() + -- TODO handle timeout error differently? Like via an error? local client_responses = lsp.buf_request_sync(bufnr, 'textDocument/completion', params) or {} local matches = {} @@ -889,12 +881,15 @@ function lsp.omnifunc(findstart, base) -- TODO how to handle errors? if not response.error then local data = response.result - local completion_items = util.text_document_completion_list_to_complete_items(data or {}, line_to_cursor) + local completion_items = util.text_document_completion_list_to_complete_items(data or {}) local _ = log.trace() and log.trace("omnifunc.completion_items", completion_items) vim.list_extend(matches, completion_items) end end - return matches + + -- Instead of returning matches call complete instead + vim.fn.complete(textMatch+1, matches) + return {} end end diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 3798b30a46..8f27353698 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -19,21 +19,6 @@ local function npcall(fn, ...) return ok_or_nil(pcall(fn, ...)) end ---- Find the longest shared prefix between prefix and word. --- e.g. remove_prefix("123tes", "testing") == "ting" -local function remove_prefix(prefix, word) - local max_prefix_length = math.min(#prefix, #word) - local prefix_length = 0 - for i = 1, max_prefix_length do - local current_line_suffix = prefix:sub(-i) - local word_prefix = word:sub(1, i) - if current_line_suffix == word_prefix then - prefix_length = i - end - end - return word:sub(prefix_length + 1) -end - -- TODO(ashkan) @performance this could do less copying. function M.set_lines(lines, A, B, new_lines) -- 0-indexing to 1-indexing @@ -161,15 +146,11 @@ end --- Getting vim complete-items with incomplete flag. -- @params CompletionItem[], CompletionList or nil (https://microsoft.github.io/language-server-protocol/specification#textDocument_completion) -- @return { matches = complete-items table, incomplete = boolean } -function M.text_document_completion_list_to_complete_items(result, line_prefix) +function M.text_document_completion_list_to_complete_items(result) local items = M.extract_completion_items(result) if vim.tbl_isempty(items) then return {} end - -- Only initialize if we have some items. - if not line_prefix then - line_prefix = M.get_current_line_to_cursor() - end local matches = {} @@ -187,10 +168,8 @@ function M.text_document_completion_list_to_complete_items(result, line_prefix) end local word = completion_item.insertText or completion_item.label - - -- Ref: `:h complete-items` table.insert(matches, { - word = remove_prefix(line_prefix, word), + word = word, abbr = completion_item.label, kind = protocol.CompletionItemKind[completion_item.kind] or '', menu = completion_item.detail or '', -- cgit From 026ba804d173c41ab99ee270c93f7975c1d6d713 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Fri, 20 Dec 2019 02:50:37 -0800 Subject: LSP: Improve the display of the default hover callback. (#11576) Strips the code blocks from markdown and does syntax highlighting. --- runtime/lua/vim/lsp/callbacks.lua | 14 ++-- runtime/lua/vim/lsp/util.lua | 136 +++++++++++++++++++++++++++++++++++--- 2 files changed, 138 insertions(+), 12 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/callbacks.lua b/runtime/lua/vim/lsp/callbacks.lua index 4fc3f74519..a3cd521b86 100644 --- a/runtime/lua/vim/lsp/callbacks.lua +++ b/runtime/lua/vim/lsp/callbacks.lua @@ -68,16 +68,22 @@ M['textDocument/completion'] = function(_, _, result) end M['textDocument/hover'] = function(_, method, result) - util.focusable_preview(method, function() + util.focusable_float(method, function() if not (result and result.contents) then - return { 'No information available' } + -- return { 'No information available' } + return end local markdown_lines = util.convert_input_to_markdown_lines(result.contents) markdown_lines = util.trim_empty_lines(markdown_lines) if vim.tbl_isempty(markdown_lines) then - return { 'No information available' } + -- return { 'No information available' } + return end - return markdown_lines, util.try_trim_markdown_code_blocks(markdown_lines) + local bufnr, winnr = util.fancy_floating_markdown(markdown_lines, { + pad_left = 1; pad_right = 1; + }) + util.close_preview_autocmd({"CursorMoved", "BufHidden", "InsertCharPre"}, winnr) + return bufnr, winnr end) end diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 8f27353698..4a781359d4 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -19,11 +19,13 @@ local function npcall(fn, ...) return ok_or_nil(pcall(fn, ...)) end --- TODO(ashkan) @performance this could do less copying. function M.set_lines(lines, A, B, new_lines) -- 0-indexing to 1-indexing local i_0 = A[1] + 1 - local i_n = B[1] + 1 + -- If it extends past the end, truncate it to the end. This is because the + -- way the LSP describes the range including the last newline is by + -- specifying a line number after what we would call the last line. + local i_n = math.min(B[1] + 1, #lines) if not (i_0 >= 1 and i_0 <= #lines and i_n >= 1 and i_n <= #lines) then error("Invalid range: "..vim.inspect{A = A; B = B; #lines, new_lines}) end @@ -88,7 +90,7 @@ function M.apply_text_edits(text_edits, bufnr) table.sort(cleaned, edit_sort_key) local lines = api.nvim_buf_get_lines(bufnr, start_line, finish_line + 1, false) local fix_eol = api.nvim_buf_get_option(bufnr, 'fixeol') - local set_eol = fix_eol and api.nvim_buf_line_count(bufnr) == finish_line + 1 + local set_eol = fix_eol and api.nvim_buf_line_count(bufnr) <= finish_line + 1 if set_eol and #lines[#lines] ~= 0 then table.insert(lines, '') end @@ -315,9 +317,9 @@ end -- Check if a window with `unique_name` tagged is associated with the current -- buffer. If not, make a new preview. -- --- fn()'s return values will be passed directly to open_floating_preview in the +-- fn()'s return bufnr, winnr -- case that a new floating window should be created. -function M.focusable_preview(unique_name, fn) +function M.focusable_float(unique_name, fn) if npcall(api.nvim_win_get_var, 0, unique_name) then return api.nvim_command("wincmd p") end @@ -330,9 +332,127 @@ function M.focusable_preview(unique_name, fn) return end end - local pbufnr, pwinnr = M.open_floating_preview(fn()) - api.nvim_win_set_var(pwinnr, unique_name, bufnr) - return pbufnr, pwinnr + local pbufnr, pwinnr = fn() + if pbufnr then + api.nvim_win_set_var(pwinnr, unique_name, bufnr) + return pbufnr, pwinnr + end +end + +-- Check if a window with `unique_name` tagged is associated with the current +-- buffer. If not, make a new preview. +-- +-- fn()'s return values will be passed directly to open_floating_preview in the +-- case that a new floating window should be created. +function M.focusable_preview(unique_name, fn) + return M.focusable_float(unique_name, function() + return M.open_floating_preview(fn()) + end) +end + +-- Convert markdown into syntax highlighted regions by stripping the code +-- blocks and converting them into highlighted code. +-- This will by default insert a blank line separator after those code block +-- regions to improve readability. +function M.fancy_floating_markdown(contents, opts) + local pad_left = opts and opts.pad_left + local pad_right = opts and opts.pad_right + local stripped = {} + local highlights = {} + do + local i = 1 + while i <= #contents do + local line = contents[i] + -- TODO(ashkan): use a more strict regex for filetype? + local ft = line:match("^```([a-zA-Z0-9_]*)$") + -- local ft = line:match("^```(.*)$") + -- TODO(ashkan): validate the filetype here. + if ft then + local start = #stripped + i = i + 1 + while i <= #contents do + line = contents[i] + if line == "```" then + i = i + 1 + break + end + table.insert(stripped, line) + i = i + 1 + end + table.insert(highlights, { + ft = ft; + start = start + 1; + finish = #stripped + 1 - 1; + }) + else + table.insert(stripped, line) + i = i + 1 + end + end + end + local width = 0 + for i, v in ipairs(stripped) do + v = v:gsub("\r", "") + if pad_left then v = (" "):rep(pad_left)..v end + if pad_right then v = v..(" "):rep(pad_right) end + stripped[i] = v + width = math.max(width, #v) + end + if opts and opts.max_width then + width = math.min(opts.max_width, width) + end + -- TODO(ashkan): decide how to make this customizable. + local insert_separator = true + if insert_separator then + for i, h in ipairs(highlights) do + h.start = h.start + i - 1 + h.finish = h.finish + i - 1 + if h.finish + 1 <= #stripped then + table.insert(stripped, h.finish + 1, string.rep("─", width)) + end + end + end + + -- Make the floating window. + local height = #stripped + local bufnr = api.nvim_create_buf(false, true) + local winnr = api.nvim_open_win(bufnr, false, M.make_floating_popup_options(width, height, opts)) + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, stripped) + + -- Switch to the floating window to apply the syntax highlighting. + -- This is because the syntax command doesn't accept a target. + local cwin = vim.api.nvim_get_current_win() + vim.api.nvim_set_current_win(winnr) + + vim.cmd("ownsyntax markdown") + local idx = 1 + local function highlight_region(ft, start, finish) + if ft == '' then return end + local name = ft..idx + idx = idx + 1 + local lang = "@"..ft:upper() + -- TODO(ashkan): better validation before this. + if not pcall(vim.cmd, string.format("syntax include %s syntax/%s.vim", lang, ft)) then + return + end + vim.cmd(string.format("syntax region %s start=+\\%%%dl+ end=+\\%%%dl+ contains=%s", name, start, finish + 1, lang)) + end + -- Previous highlight region. + -- TODO(ashkan): this wasn't working for some reason, but I would like to + -- make sure that regions between code blocks are definitely markdown. + -- local ph = {start = 0; finish = 1;} + for _, h in ipairs(highlights) do + -- highlight_region('markdown', ph.finish, h.start) + highlight_region(h.ft, h.start, h.finish) + -- ph = h + end + + vim.api.nvim_set_current_win(cwin) + return bufnr, winnr +end + +function M.close_preview_autocmd(events, winnr) + api.nvim_command("autocmd "..table.concat(events, ',').." ++once lua pcall(vim.api.nvim_win_close, "..winnr..", true)") end function M.open_floating_preview(contents, filetype, opts) -- cgit From ee7ac469c6f0f15a2d75991ef053a18d93e01756 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Fri, 20 Dec 2019 22:49:29 -0800 Subject: LSP: Use async completion for omnifunc. (#11578) --- runtime/lua/vim/lsp.lua | 55 ++++++++++++++++----------------------- runtime/lua/vim/lsp/callbacks.lua | 5 ++-- 2 files changed, 26 insertions(+), 34 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 501cc6f670..0ecf57f50c 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -858,39 +858,30 @@ function lsp.omnifunc(findstart, base) end end - if findstart == 1 then - -- First, just return the current cursor column, we only really need that - return vim.fn.col('.') - else - -- Then, perform standard completion request - log.info("base ", base) - - local pos = vim.api.nvim_win_get_cursor(0) - local line = vim.api.nvim_get_current_line() - local line_to_cursor = line:sub(1, pos[2]) - local _ = log.trace() and log.trace("omnifunc.line", pos, line) - - -- Get the start postion of the current keyword - local textMatch = vim.fn.match(line_to_cursor, '\\k*$') - local params = util.make_position_params() - - -- TODO handle timeout error differently? Like via an error? - local client_responses = lsp.buf_request_sync(bufnr, 'textDocument/completion', params) or {} - local matches = {} - for _, response in pairs(client_responses) do - -- TODO how to handle errors? - if not response.error then - local data = response.result - local completion_items = util.text_document_completion_list_to_complete_items(data or {}) - local _ = log.trace() and log.trace("omnifunc.completion_items", completion_items) - vim.list_extend(matches, completion_items) - end - end + -- Then, perform standard completion request + local _ = log.info() and log.info("base ", base) + + local pos = vim.api.nvim_win_get_cursor(0) + local line = vim.api.nvim_get_current_line() + local line_to_cursor = line:sub(1, pos[2]) + local _ = log.trace() and log.trace("omnifunc.line", pos, line) + + -- Get the start postion of the current keyword + local textMatch = vim.fn.match(line_to_cursor, '\\k*$') + local params = util.make_position_params() + + local items = {} + lsp.buf_request(bufnr, 'textDocument/completion', params, function(err, _, result) + if err or not result then return end + local matches = util.text_document_completion_list_to_complete_items(result) + -- TODO(ashkan): is this the best way to do this? + vim.list_extend(items, matches) + vim.fn.complete(textMatch+1, items) + end) - -- Instead of returning matches call complete instead - vim.fn.complete(textMatch+1, matches) - return {} - end + -- Return -2 to signal that we should continue completion so that we can + -- async complete. + return -2 end function lsp.client_is_stopped(client_id) diff --git a/runtime/lua/vim/lsp/callbacks.lua b/runtime/lua/vim/lsp/callbacks.lua index a3cd521b86..794140ee2e 100644 --- a/runtime/lua/vim/lsp/callbacks.lua +++ b/runtime/lua/vim/lsp/callbacks.lua @@ -62,9 +62,10 @@ M['textDocument/completion'] = function(_, _, result) local row, col = unpack(api.nvim_win_get_cursor(0)) local line = assert(api.nvim_buf_get_lines(0, row-1, row, false)[1]) local line_to_cursor = line:sub(col+1) + local textMatch = vim.fn.match(line_to_cursor, '\\k*$') - local matches = util.text_document_completion_list_to_complete_items(result, line_to_cursor) - vim.fn.complete(col, matches) + local matches = util.text_document_completion_list_to_complete_items(result) + vim.fn.complete(textMatch+1, matches) end M['textDocument/hover'] = function(_, method, result) -- cgit From 440695c29696f261337227e5c419aa1cf313c2dd Mon Sep 17 00:00:00 2001 From: Björn Linse Date: Sat, 28 Sep 2019 14:27:20 +0200 Subject: tree-sitter: implement query functionality and highlighting prototype [skip.lint] --- runtime/lua/vim/treesitter.lua | 120 +++++++++++++++++++++++++++++--- runtime/lua/vim/tshighlighter.lua | 142 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 254 insertions(+), 8 deletions(-) create mode 100644 runtime/lua/vim/tshighlighter.lua (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua index e0202927bb..aa8b8fcdd1 100644 --- a/runtime/lua/vim/treesitter.lua +++ b/runtime/lua/vim/treesitter.lua @@ -12,9 +12,13 @@ function Parser:parse() if self.valid then return self.tree end - self.tree = self._parser:parse_buf(self.bufnr) + local changes + self.tree, changes = self._parser:parse_buf(self.bufnr) self.valid = true - return self.tree + for _, cb in ipairs(self.change_cbs) do + cb(changes) + end + return self.tree, changes end function Parser:_on_lines(bufnr, _, start_row, old_stop_row, stop_row, old_byte_size) @@ -26,17 +30,28 @@ function Parser:_on_lines(bufnr, _, start_row, old_stop_row, stop_row, old_byte_ self.valid = false end -local module = { +local M = { add_language=vim._ts_add_language, inspect_language=vim._ts_inspect_language, + parse_query = vim._ts_parse_query, } -function module.create_parser(bufnr, ft, id) +setmetatable(M, { + __index = function (t, k) + if k == "TSHighlighter" then + t[k] = require'vim.tshighlighter' + return t[k] + end + end + }) + +function M.create_parser(bufnr, ft, id) if bufnr == 0 then bufnr = a.nvim_get_current_buf() end - local self = setmetatable({bufnr=bufnr, valid=false}, Parser) + local self = setmetatable({bufnr=bufnr, lang=ft, valid=false}, Parser) self._parser = vim._create_ts_parser(ft) + self.change_cbs = {} self:parse() -- TODO(bfredl): use weakref to self, so that the parser is free'd is no plugin is -- using it. @@ -55,7 +70,7 @@ function module.create_parser(bufnr, ft, id) return self end -function module.get_parser(bufnr, ft) +function M.get_parser(bufnr, ft, cb) if bufnr == nil or bufnr == 0 then bufnr = a.nvim_get_current_buf() end @@ -65,9 +80,98 @@ function module.get_parser(bufnr, ft) local id = tostring(bufnr)..'_'..ft if parsers[id] == nil then - parsers[id] = module.create_parser(bufnr, ft, id) + parsers[id] = M.create_parser(bufnr, ft, id) + end + if cb ~= nil then + table.insert(parsers[id].change_cbs, cb) end return parsers[id] end -return module +-- query: pattern matching on trees +-- predicate matching is implemented in lua +local Query = {} +Query.__index = Query + +function M.parse_query(lang, query) + local self = setmetatable({}, Query) + self.query = vim._ts_parse_query(lang, query) + self.info = self.query:inspect() + self.captures = self.info.captures + return self +end + +local function get_node_text(node, bufnr) + local start_row, start_col, end_row, end_col = node:range() + if start_row ~= end_row then + return nil + end + local line = a.nvim_buf_get_lines(bufnr, start_row, start_row+1, true)[1] + return string.sub(line, start_col+1, end_col) +end + +local function match_preds(match, preds, bufnr) + for _, pred in pairs(preds) do + if pred[1] == "eq?" then + local node = match[pred[2]] + local node_text = get_node_text(node, bufnr) + + local str + if type(pred[3]) == "string" then + -- (eq? @aa "foo") + str = pred[3] + else + -- (eq? @aa @bb) + str = get_node_text(match[pred[3]], bufnr) + end + + if node_text ~= str or str == nil then + return false + end + else + return false + end + end + return true +end + +function Query:iter_captures(node, bufnr, start, stop) + if bufnr == 0 then + bufnr = vim.api.nvim_get_current_buf() + end + local raw_iter = node:_rawquery(self.query,true,start,stop) + local function iter() + local capture, captured_node, match = raw_iter() + if match ~= nil then + local preds = self.info.patterns[match.pattern] + local active = match_preds(match, preds, bufnr) + match.active = active + if not active then + return iter() -- tail call: try next match + end + end + return capture, captured_node + end + return iter +end + +function Query:iter_matches(node, bufnr, start, stop) + if bufnr == 0 then + bufnr = vim.api.nvim_get_current_buf() + end + local raw_iter = node:_rawquery(self.query,false,start,stop) + local function iter() + local pattern, match = raw_iter() + if match ~= nil then + local preds = self.info.patterns[pattern] + local active = (not preds) or match_preds(match, preds, bufnr) + if not active then + return iter() -- tail call: try next match + end + end + return pattern, match + end + return iter +end + +return M diff --git a/runtime/lua/vim/tshighlighter.lua b/runtime/lua/vim/tshighlighter.lua new file mode 100644 index 0000000000..1544ecbf49 --- /dev/null +++ b/runtime/lua/vim/tshighlighter.lua @@ -0,0 +1,142 @@ +local a = vim.api + +-- support reload for quick experimentation +local TSHighlighter = rawget(vim.treesitter, 'TSHighlighter') or {} +TSHighlighter.__index = TSHighlighter + +-- These are conventions defined by tree-sitter, though it +-- needs to be user extensible also. +-- TODO(bfredl): this is very much incomplete, we will need to +-- go through a few tree-sitter provided queries and decide +-- on translations that makes the most sense. +TSHighlighter.hl_map = { + keyword="Keyword", + string="String", + type="Type", + comment="Comment", + constant="Constant", + operator="Operator", + number="Number", + label="Label", + ["function"]="Function", + ["function.special"]="Function", +} + +function TSHighlighter.new(query, bufnr, ft) + local self = setmetatable({}, TSHighlighter) + self.parser = vim.treesitter.get_parser(bufnr, ft, function(...) self:on_change(...) end) + self.buf = self.parser.bufnr + -- TODO(bfredl): perhaps on_start should be called uncondionally, instead for only on mod? + local tree = self.parser:parse() + self.root = tree:root() + self:set_query(query) + self.edit_count = 0 + self.redraw_count = 0 + self.line_count = {} + a.nvim_buf_set_option(self.buf, "syntax", "") + a.nvim__buf_set_luahl(self.buf, { + on_start=function(...) return self:on_start(...) end, + on_window=function(...) return self:on_window(...) end, + on_line=function(...) return self:on_line(...) end, + }) + + -- Tricky: if syntax hasn't been enabled, we need to reload color scheme + -- but use synload.vim rather than syntax.vim to not enable + -- syntax FileType autocmds. Later on we should integrate with the + -- `:syntax` and `set syntax=...` machinery properly. + if vim.g.syntax_on ~= 1 then + vim.api.nvim_command("runtime! syntax/synload.vim") + end + return self +end + +function TSHighlighter:set_query(query) + if type(query) == "string" then + query = vim.treesitter.parse_query(self.parser.lang, query) + end + self.query = query + + self.id_map = {} + for i, capture in ipairs(self.query.captures) do + local hl = 0 + local firstc = string.sub(capture, 1, 1) + local hl_group = self.hl_map[capture] + if firstc ~= string.lower(firstc) then + hl_group = vim.split(capture, '.', true)[1] + end + if hl_group then + hl = a.nvim_get_hl_id_by_name(hl_group) + end + self.id_map[i] = hl + end +end + +function TSHighlighter:on_change(changes) + for _, ch in ipairs(changes or {}) do + a.nvim__buf_redraw_range(self.buf, ch[1], ch[3]+1) + end + self.edit_count = self.edit_count + 1 +end + +function TSHighlighter:on_start(_, _buf, _tick) + local tree = self.parser:parse() + self.root = tree:root() +end + +function TSHighlighter:on_window(_, _win, _buf, _topline, botline) + self.iter = nil + self.active_nodes = {} + self.nextrow = 0 + self.botline = botline + self.redraw_count = self.redraw_count + 1 +end + +function TSHighlighter:on_line(_, _win, buf, line) + if self.iter == nil then + self.iter = self.query:iter_captures(self.root,buf,line,self.botline) + end + while line >= self.nextrow do + local capture, node, match = self.iter() + local active = true + if capture == nil then + break + end + if match ~= nil then + active = self:run_pred(match) + match.active = active + end + local start_row, start_col, end_row, end_col = node:range() + local hl = self.id_map[capture] + if hl > 0 and active then + if start_row == line and end_row == line then + a.nvim__put_attr(hl, start_col, end_col) + elseif end_row >= line then + -- TODO(bfredl): this is quite messy. Togheter with multiline bufhl we should support + -- luahl generating multiline highlights (and other kinds of annotations) + self.active_nodes[{hl=hl, start_row=start_row, start_col=start_col, end_row=end_row, end_col=end_col}] = true + end + end + if start_row > line then + self.nextrow = start_row + end + end + for node,_ in pairs(self.active_nodes) do + if node.start_row <= line and node.end_row >= line then + local start_col, end_col = node.start_col, node.end_col + if node.start_row < line then + start_col = 0 + end + if node.end_row > line then + end_col = 9000 + end + a.nvim__put_attr(node.hl, start_col, end_col) + end + if node.end_row <= line then + self.active_nodes[node] = nil + end + end + self.line_count[line] = (self.line_count[line] or 0) + 1 + --return tostring(self.line_count[line]) +end + +return TSHighlighter -- cgit From 07a2260e1d5fa8c7c4e2ec1ff8679a120fe399e8 Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Tue, 24 Dec 2019 14:28:09 -0800 Subject: LSP: Handle rpc RequestCancelled specifically. (#11606) This was creating extra noise in errors that we should've been handling internally. Fixes #11515 --- runtime/lua/vim/lsp/rpc.lua | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua index a558f66a42..72a0bf8d6f 100644 --- a/runtime/lua/vim/lsp/rpc.lua +++ b/runtime/lua/vim/lsp/rpc.lua @@ -1,3 +1,4 @@ +local vim = vim local uv = vim.loop local log = require('vim.lsp.log') local protocol = require('vim.lsp.protocol') @@ -377,6 +378,22 @@ local function create_and_start_client(cmd, cmd_args, handlers, extra_spawn_para decoded.error = convert_NIL(decoded.error) decoded.result = convert_NIL(decoded.result) + -- Do not surface RequestCancelled to users, it is RPC-internal. + if decoded.error + and decoded.error.code == protocol.ErrorCodes.RequestCancelled then + local _ = log.debug() and log.debug("Received cancellation ack", decoded) + local result_id = tonumber(decoded.id) + -- Clear any callback since this is cancelled now. + -- This is safe to do assuming that these conditions hold: + -- - The server will not send a result callback after this cancellation. + -- - If the server sent this cancellation ACK after sending the result, the user of this RPC + -- client will ignore the result themselves. + if result_id then + message_callbacks[result_id] = nil + end + return + end + -- We sent a number, so we expect a number. local result_id = tonumber(decoded.id) local callback = message_callbacks[result_id] -- cgit From 680693e263576e34d5947c43ab0ae3ff0ebfeab5 Mon Sep 17 00:00:00 2001 From: Hirokazu Hata Date: Sun, 29 Dec 2019 02:28:00 +0900 Subject: runtime: Add vim.lsp.get_client_by_name (#11603) Since the client name is more obvious than the client id for the user, add an API to get the lsp client by the client name. --- runtime/lua/vim/lsp.lua | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 0ecf57f50c..042ed7bcfe 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -697,6 +697,16 @@ function lsp.get_client_by_id(client_id) return active_clients[client_id] end +-- Look up an active client by its name, returns nil if it is not yet initialized +-- or is not a valid name. +-- @param client_name string the client name. +function lsp.get_client_by_name(client_name) + for _, client in pairs(active_clients) do + if client.name == client_name then return client end + end + return nil +end + -- Stop a client by its id, optionally with force. -- You can also use the `stop()` function on a client if you already have -- access to it. -- cgit From 34a59242a0d42687a49119cca590e7b4203496ef Mon Sep 17 00:00:00 2001 From: Ashkan Kiani Date: Sun, 29 Dec 2019 00:05:32 -0800 Subject: Revert "runtime: Add vim.lsp.get_client_by_name" #11623 reverts 680693e263576e34d5947c43ab0ae3ff0ebfeab5 #11603 --- runtime/lua/vim/lsp.lua | 10 ---------- 1 file changed, 10 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 042ed7bcfe..0ecf57f50c 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -697,16 +697,6 @@ function lsp.get_client_by_id(client_id) return active_clients[client_id] end --- Look up an active client by its name, returns nil if it is not yet initialized --- or is not a valid name. --- @param client_name string the client name. -function lsp.get_client_by_name(client_name) - for _, client in pairs(active_clients) do - if client.name == client_name then return client end - end - return nil -end - -- Stop a client by its id, optionally with force. -- You can also use the `stop()` function on a client if you already have -- access to it. -- cgit From b112fe828fd2457692f556626d7657615e53cb0b Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Tue, 31 Dec 2019 06:52:14 -0800 Subject: gen_vimdoc.py: generate LSP docs --- runtime/lua/vim/lsp.lua | 394 ++++++++++++++++++++++++--------------- runtime/lua/vim/lsp/protocol.lua | 4 + runtime/lua/vim/lsp/rpc.lua | 7 +- 3 files changed, 249 insertions(+), 156 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 0ecf57f50c..a14c432f0c 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -204,95 +204,155 @@ local function text_document_did_open_handler(bufnr, client) client.notify('textDocument/didOpen', params) end +--- LSP client object. +--- +--- - Methods: +--- +--- - request(method, params, [callback]) +--- Send a request to the server. If callback is not specified, it will use +--- {client.callbacks} to try to find a callback. If one is not found there, +--- then an error will occur. +--- This is a thin wrapper around {client.rpc.request} with some additional +--- checking. +--- Returns a boolean to indicate if the notification was successful. If it +--- is false, then it will always be false (the client has shutdown). +--- If it was successful, then it will return the request id as the second +--- result. You can use this with `notify("$/cancel", { id = request_id })` +--- to cancel the request. This helper is made automatically with +--- |vim.lsp.buf_request()| +--- Returns: status, [client_id] +--- +--- - notify(method, params) +--- This is just {client.rpc.notify}() +--- Returns a boolean to indicate if the notification was successful. If it +--- is false, then it will always be false (the client has shutdown). +--- Returns: status +--- +--- - cancel_request(id) +--- This is just {client.rpc.notify}("$/cancelRequest", { id = id }) +--- Returns the same as `notify()`. +--- +--- - stop([force]) +--- Stop a client, optionally with force. +--- By default, it will just ask the server to shutdown without force. +--- If you request to stop a client which has previously been requested to +--- shutdown, it will automatically escalate and force shutdown. +--- +--- - is_stopped() +--- Returns true if the client is fully stopped. +--- +--- - Members +--- - id (number): The id allocated to the client. +--- +--- - name (string): If a name is specified on creation, that will be +--- used. Otherwise it is just the client id. This is used for +--- logs and messages. +--- +--- - offset_encoding (string): The encoding used for communicating +--- with the server. You can modify this in the `on_init` method +--- before text is sent to the server. +--- +--- - callbacks (table): The callbacks used by the client as +--- described in |lsp-callbacks|. +--- +--- - config (table): copy of the table that was passed by the user +--- to |vim.lsp.start_client()|. +--- +--- - server_capabilities (table): Response from the server sent on +--- `initialize` describing the server's capabilities. +--- +--- - resolved_capabilities (table): Normalized table of +--- capabilities that we have detected based on the initialize +--- response from the server in `server_capabilities`. +function lsp.client() + error() +end --- Start a client and initialize it. --- Its arguments are passed via a configuration object. --- --- Mandatory parameters: --- --- root_dir: {string} specifying the directory where the LSP server will base --- as its rootUri on initialization. --- --- cmd: {string} or {list} which is the base command to execute for the LSP. A --- string will be run using |'shell'| and a list will be interpreted as a bare --- command with arguments passed. This is the same as |jobstart()|. --- --- Optional parameters: - --- cmd_cwd: {string} specifying the directory to launch the `cmd` process. This --- is not related to `root_dir`. By default, |getcwd()| is used. --- --- cmd_env: {table} specifying the environment flags to pass to the LSP on --- spawn. This can be specified using keys like a map or as a list with `k=v` --- pairs or both. Non-string values are coerced to a string. --- For example: `{ "PRODUCTION=true"; "TEST=123"; PORT = 8080; HOST = "0.0.0.0"; }`. --- --- capabilities: A {table} which will be used instead of --- `vim.lsp.protocol.make_client_capabilities()` which contains neovim's --- default capabilities and passed to the language server on initialization. --- You'll probably want to use make_client_capabilities() and modify the --- result. --- NOTE: --- To send an empty dictionary, you should use --- `{[vim.type_idx]=vim.types.dictionary}` Otherwise, it will be encoded as --- an array. --- --- callbacks: A {table} of whose keys are language server method names and the --- values are `function(err, method, params, client_id)`. --- This will be called for: --- - notifications from the server, where `err` will always be `nil` --- - requests initiated by the server. For these, you can respond by returning --- two values: `result, err`. The err must be in the format of an RPC error, --- which is `{ code, message, data? }`. You can use |vim.lsp.rpc_response_error()| --- to help with this. --- - as a callback for requests initiated by the client if the request doesn't --- explicitly specify a callback. --- --- init_options: A {table} of values to pass in the initialization request --- as `initializationOptions`. See the `initialize` in the LSP spec. --- --- name: A {string} used in log messages. Defaults to {client_id} --- --- offset_encoding: One of 'utf-8', 'utf-16', or 'utf-32' which is the --- encoding that the LSP server expects. By default, it is 'utf-16' as --- specified in the LSP specification. The client does not verify this --- is correct. --- --- on_error(code, ...): A function for handling errors thrown by client --- operation. {code} is a number describing the error. Other arguments may be --- passed depending on the error kind. @see |vim.lsp.client_errors| for --- possible errors. `vim.lsp.client_errors[code]` can be used to retrieve a --- human understandable string. --- --- before_init(initialize_params, config): A function which is called *before* --- the request `initialize` is completed. `initialize_params` contains --- the parameters we are sending to the server and `config` is the config that --- was passed to `start_client()` for convenience. You can use this to modify --- parameters before they are sent. --- --- on_init(client, initialize_result): A function which is called after the --- request `initialize` is completed. `initialize_result` contains --- `capabilities` and anything else the server may send. For example, `clangd` --- sends `result.offsetEncoding` if `capabilities.offsetEncoding` was sent to --- it. --- --- on_exit(code, signal, client_id): A function which is called after the --- client has exited. code is the exit code of the process, and signal is a --- number describing the signal used to terminate (if any). --- --- on_attach(client, bufnr): A function which is called after the client is --- attached to a buffer. --- --- trace: 'off' | 'messages' | 'verbose' | nil passed directly to the language --- server in the initialize request. Invalid/empty values will default to 'off' --- --- @returns client_id You can use |vim.lsp.get_client_by_id()| to get the --- actual client. --- --- NOTE: The client is only available *after* it has been initialized, which --- may happen after a small delay (or never if there is an error). --- For this reason, you may want to use `on_init` to do any actions once the --- client has been initialized. +--- Its arguments are passed via a configuration object. +--- +--- Mandatory parameters: +--- +--- root_dir: {string} specifying the directory where the LSP server will base +--- as its rootUri on initialization. +--- +--- cmd: {string} or {list} which is the base command to execute for the LSP. A +--- string will be run using 'shell' and a list will be interpreted as a bare +--- command with arguments passed. This is the same as |jobstart()|. +--- +--@param cmd_cwd: {string} specifying the directory to launch the `cmd` process. This +--- is not related to `root_dir`. By default, |getcwd()| is used. +--- +--@param cmd_env: {table} specifying the environment flags to pass to the LSP on +--- spawn. This can be specified using keys like a map or as a list with `k=v` +--- pairs or both. Non-string values are coerced to a string. +--- For example: `{ "PRODUCTION=true"; "TEST=123"; PORT = 8080; HOST = "0.0.0.0"; }`. +--- +--@param capabilities: Map overriding the default capabilities defined by +--- |vim.lsp.protocol.make_client_capabilities()|, passed to the language +--- server on initialization. Hint: use make_client_capabilities() and modify +--- its result. +--- - Note: To send an empty dictionary use +--- `{[vim.type_idx]=vim.types.dictionary}`, else it will be encoded as an +--- array. +--- +--@param callbacks: Map of language server method names to +--- `function(err, method, params, client_id)` handler. +--- Invoked for: +--- - Notifications from the server, where `err` will always be `nil`. +--- - Requests initiated by the server. For these you can respond by returning +--- two values: `result, err` where err must be shaped like a RPC error, +--- i.e. `{ code, message, data? }`. Use |vim.lsp.rpc_response_error()| to +--- help with this. +--- - Default callback for client requests not explicitly specifying +--- a callback. +--- +--@param init_options values to pass in the initialization request +--- as `initializationOptions`. See `initialize` in the LSP spec. +--- +--@param name: string used in log messages. Defaults to {client_id} +--- +--@param offset_encoding: One of "utf-8", "utf-16", or "utf-32" which is the +--- encoding that the LSP server expects. By default, it is "utf-16" as +--- specified in the LSP specification. The client does not verify this +--- is correct. +--- +--@param on_error Callback with parameters (code, ...), invoked +--- when the client operation throws an error. +--- {code} is a number describing the error. Other arguments may be +--- passed depending on the error kind. See |vim.lsp.client_errors| for +--- possible errors. Use `vim.lsp.client_errors[code]` to get human-friendly +--- name. +--- +--@param before_init Callback with parameters (initialize_params, config) invoked +--- before the LSP "initialize" phase, where `params` contains the +--- parameters being sent to the server and `config` is the config +--- that was passed to `start_client()`. You can use this to modify +--- parameters before they are sent. +--- +--@param on_init Callback (client, initialize_result) invoked after LSP +--- "initialize", where `result` is a table of `capabilities` and +--- anything else the server may send. For example, clangd sends +--- `initialize_result.offsetEncoding` if `capabilities.offsetEncoding` was +--- sent to it. You can only modify the `client.offset_encoding` here before +--- any notifications are sent. +--- +--@param on_exit Callback (code, signal, client_id) invoked on client +--- exit. +--- - code: exit code of the process +--- - signal: number describing the signal used to terminate (if any) +--- - client_id: client handle +--- +--@param on_attach Callback (client, bufnr) invoked when client +--- attaches to a buffer. +--- +--@param trace: "off" | "messages" | "verbose" | nil passed directly to the language +--- server in the initialize request. Invalid/empty values will default to "off" +--- +--@returns Client id. |vim.lsp.get_client_by_id()| Note: client is only +--- available after it has been initialized, which may happen after a small +--- delay (or never if there is an error). Use `on_init` to do any actions once +--- the client has been initialized. function lsp.start_client(config) local cleaned_config = validate_client_config(config) local cmd, cmd_args, offset_encoding = cleaned_config.cmd, cleaned_config.cmd_args, cleaned_config.offset_encoding @@ -402,8 +462,8 @@ function lsp.start_client(config) initializationOptions = config.init_options; -- The capabilities provided by the client (editor or tool) capabilities = config.capabilities or protocol.make_client_capabilities(); - -- The initial trace setting. If omitted trace is disabled ('off'). - -- trace = 'off' | 'messages' | 'verbose'; + -- The initial trace setting. If omitted trace is disabled ("off"). + -- trace = "off" | "messages" | "verbose"; trace = valid_traces[config.trace] or 'off'; -- The workspace folders configured in the client when the server starts. -- This property is only available if the client supports workspace folders. @@ -634,10 +694,13 @@ function lsp._text_document_did_save_handler(bufnr) end) end --- Implements the textDocument/did* notifications required to track a buffer --- for any language server. --- @param bufnr [number] buffer handle or 0 for current --- @param client_id [number] the client id +--- Implements the `textDocument/did…` notifications required to track a buffer +--- for any language server. +--- +--- Without calling this, the server won't be notified of changes to a buffer. +--- +--- @param bufnr (number) Buffer handle, or 0 for current +--- @param client_id (number) Client id function lsp.buf_attach_client(bufnr, client_id) validate { bufnr = {bufnr, 'n', true}; @@ -683,28 +746,33 @@ function lsp.buf_attach_client(bufnr, client_id) return true end --- Check if a buffer is attached for a particular client. --- @param bufnr [number] buffer handle or 0 for current --- @param client_id [number] the client id +--- Checks if a buffer is attached for a particular client. +--- +---@param bufnr (number) Buffer handle, or 0 for current +---@param client_id (number) the client id function lsp.buf_is_attached(bufnr, client_id) return (all_buffer_active_clients[bufnr] or {})[client_id] == true end --- Look up an active client by its id, returns nil if it is not yet initialized --- or is not a valid id. --- @param client_id number the client id. +--- Gets an active client by id, or nil if the id is invalid or the +--- client is not yet initialized. +--- +--@param client_id client id number +--- +--@return |vim.lsp.client| object, or nil function lsp.get_client_by_id(client_id) return active_clients[client_id] end --- Stop a client by its id, optionally with force. --- You can also use the `stop()` function on a client if you already have --- access to it. --- By default, it will just ask the server to shutdown without force. --- If you request to stop a client which has previously been requested to shutdown, --- it will automatically force shutdown. --- @param client_id number the client id. --- @param force boolean (optional) whether to use force or request shutdown +--- Stops a client. +--- +--- You can also use the `stop()` function on a |vim.lsp.client| object. +--- +--- By default asks the server to shutdown, unless stop was requested +--- already for this client, then force-shutdown is attempted. +--- +--@param client_id client id number +--@param force boolean (optional) shutdown forcefully function lsp.stop_client(client_id, force) local client client = active_clients[client_id] @@ -718,18 +786,16 @@ function lsp.stop_client(client_id, force) end end --- Returns a list of all the active clients. +--- Gets all active clients. +--- +--@return Table of |vim.lsp.client| objects function lsp.get_active_clients() return vim.tbl_values(active_clients) end --- Stop all the clients, optionally with force. --- You can also use the `stop()` function on a client if you already have --- access to it. --- By default, it will just ask the server to shutdown without force. --- If you request to stop a client which has previously been requested to shutdown, --- it will automatically force shutdown. --- @param force boolean (optional) whether to use force or request shutdown +--- Stops all clients. +--- +--@param force boolean (optional) shutdown forcefully function lsp.stop_all_clients(force) for _, client in pairs(uninitialized_clients) do client.stop(true) @@ -761,17 +827,21 @@ end nvim_command("autocmd VimLeavePre * lua vim.lsp._vim_exit_handler()") ---- ---- Buffer level client functions. ---- ---- Send a request to a server and return the response --- @param bufnr [number] Buffer handle or 0 for current. --- @param method [string] Request method name --- @param params [table|nil] Parameters to send to the server --- @param callback [function|nil] Request callback (or uses the client's callbacks) +--- Sends an async request for all active clients attached to the +--- buffer. +--- +--@param bufnr (number) Buffer handle, or 0 for current. +--@param method (string) LSP method name +--@param params (optional, table) Parameters to send to the server +--@param callback (optional, functionnil) Handler +-- `function(err, method, params, client_id)` for this request. Defaults +-- to the client callback in `client.callbacks`. See |lsp-callbacks|. -- --- @returns: client_request_ids, cancel_all_requests +--@returns 2-tuple: +--- - Map of client-id:request-id pairs for all successful requests. +--- - Function which can be used to cancel all the requests. You could instead +--- iterate all clients and call their `cancel_request()` methods. function lsp.buf_request(bufnr, method, params, callback) validate { bufnr = { bufnr, 'n', true }; @@ -789,31 +859,39 @@ function lsp.buf_request(bufnr, method, params, callback) end end) - local function cancel_all_requests() + local function _cancel_all_requests() for client_id, request_id in pairs(client_request_ids) do local client = active_clients[client_id] client.cancel_request(request_id) end end - return client_request_ids, cancel_all_requests + return client_request_ids, _cancel_all_requests end ---- Send a request to a server and wait for the response. --- @param bufnr [number] Buffer handle or 0 for current. --- @param method [string] Request method name --- @param params [string] Parameters to send to the server --- @param timeout_ms [number|100] Maximum ms to wait for a result --- --- @returns: The table of {[client_id] = request_result} +--- Sends a request to a server and waits for the response. +--- +--- Calls |vim.lsp.buf_request()| but blocks Nvim while awaiting the result. +--- Parameters are the same as |vim.lsp.buf_request()| but the return result is +--- different. Wait maximum of {timeout_ms} (default 100) ms. +--- +--@param bufnr (number) Buffer handle, or 0 for current. +--@param method (string) LSP method name +--@param params (optional, table) Parameters to send to the server +--@param timeout_ms (optional, number, default=100) Maximum time in +--- milliseconds to wait for a result. +--- +--@returns Map of client_id:request_result. On timeout, cancel or error, +--- returns `(nil, err)` where `err` is a string describing the failure +--- reason. function lsp.buf_request_sync(bufnr, method, params, timeout_ms) local request_results = {} local result_count = 0 - local function callback(err, _method, result, client_id) + local function _callback(err, _method, result, client_id) request_results[client_id] = { error = err, result = result } result_count = result_count + 1 end - local client_request_ids, cancel = lsp.buf_request(bufnr, method, params, callback) + local client_request_ids, cancel = lsp.buf_request(bufnr, method, params, _callback) local expected_result_count = 0 for _ in pairs(client_request_ids) do expected_result_count = expected_result_count + 1 @@ -828,12 +906,13 @@ function lsp.buf_request_sync(bufnr, method, params, timeout_ms) return request_results end ---- Send a notification to a server --- @param bufnr [number] (optional): The number of the buffer --- @param method [string]: Name of the request method --- @param params [string]: Arguments to send to the server --- --- @returns nil +--- Sends a notification to all servers attached to the buffer. +--- +--@param bufnr (optional, number) Buffer handle, or 0 for current +--@param method (string) LSP method name +--@param params (string) Parameters to send to the server +--- +--@returns nil function lsp.buf_notify(bufnr, method, params) validate { bufnr = { bufnr, 'n', true }; @@ -888,12 +967,10 @@ function lsp.client_is_stopped(client_id) return active_clients[client_id] == nil end +--- Gets a map of client_id:client pairs for the given buffer, where each value +--- is a |vim.lsp.client| object. --- ---- Miscellaneous utilities. ---- - --- Retrieve a map from client_id to client of all active buffer clients. --- @param bufnr [number] (optional): buffer handle or 0 for current +--@param bufnr (optional, number): Buffer handle, or 0 for current function lsp.buf_get_clients(bufnr) bufnr = resolve_bufnr(bufnr) local result = {} @@ -903,8 +980,9 @@ function lsp.buf_get_clients(bufnr) return result end --- Print some debug information about the current buffer clients. --- The output of this function should not be relied upon and may change. +--- Prints debug info about the current buffer clients. +--- +--- Result of this function cannot be relied upon and may change. function lsp.buf_print_debug_info(bufnr) print(vim.inspect(lsp.buf_get_clients(bufnr))) end @@ -919,14 +997,20 @@ end -- -- Can be used to lookup the number from the name or the -- name from the number. --- Levels by name: 'trace', 'debug', 'info', 'warn', 'error' --- Level numbers begin with 'trace' at 0 +-- Levels by name: "trace", "debug", "info", "warn", "error" +-- Level numbers begin with "trace" at 0 lsp.log_levels = log.levels --- Set the log level for lsp logging. --- Levels by name: 'trace', 'debug', 'info', 'warn', 'error' --- Level numbers begin with 'trace' at 0 --- @param level [number|string] the case insensitive level name or number @see |vim.lsp.log_levels| +--- Sets the global log level for LSP logging. +--- +--- Levels by name: "trace", "debug", "info", "warn", "error" +--- Level numbers begin with "trace" at 0 +--- +--- Use `lsp.log_levels` for reverse lookup. +--- +--@see |vim.lsp.log_levels| +--- +--@param level [number|string] the case insensitive level name or number function lsp.set_log_level(level) if type(level) == 'string' or type(level) == 'number' then log.set_level(level) @@ -935,7 +1019,7 @@ function lsp.set_log_level(level) end end --- Return the path of the logfile used by the LSP client. +--- Gets the path of the logfile used by the LSP client. function lsp.get_log_path() return log.get_filename() end diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index ead90cc75a..f64b0b50e7 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -603,6 +603,8 @@ export interface WorkspaceClientCapabilities { } --]=] +--- Gets a new ClientCapabilities object describing the LSP client +--- capabilities. function protocol.make_client_capabilities() return { textDocument = { @@ -821,6 +823,8 @@ interface ServerCapabilities { experimental?: any; } --]] + +--- Creates a normalized object describing LSP server capabilities. function protocol.resolve_capabilities(server_capabilities) local general_properties = {} local text_document_sync_properties diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua index 72a0bf8d6f..e13b05610b 100644 --- a/runtime/lua/vim/lsp/rpc.lua +++ b/runtime/lua/vim/lsp/rpc.lua @@ -166,9 +166,14 @@ local function format_rpc_error(err) return table.concat(message_parts, ' ') end +--- Creates an RPC response object/table. +--- +--@param code RPC error code defined in `vim.lsp.protocol.ErrorCodes` +--@param message (optional) arbitrary message to send to server +--@param data (optional) arbitrary data to send to server local function rpc_response_error(code, message, data) -- TODO should this error or just pick a sane error (like InternalError)? - local code_name = assert(protocol.ErrorCodes[code], 'Invalid rpc error code') + local code_name = assert(protocol.ErrorCodes[code], 'Invalid RPC error code') return setmetatable({ code = code; message = message or code_name; -- cgit From 8b841196504f96a5c14126667fb4f4a0769914dd Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Tue, 31 Dec 2019 07:51:54 -0800 Subject: LSP: eliminate lsp.stop_all_clients() Reduce API surface. We don't need so many variations of functions. Too many functions means verbose, largely redundant documentation, tests, and cognitive burden. --- runtime/lua/vim/lsp.lua | 109 ++++++++++++++++++++++-------------------------- 1 file changed, 50 insertions(+), 59 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index a14c432f0c..670de7a996 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -268,27 +268,28 @@ function lsp.client() error() end ---- Start a client and initialize it. ---- Its arguments are passed via a configuration object. +--- Starts and initializes a client with the given configuration. --- ---- Mandatory parameters: +--- Parameters `cmd` and `root_dir` are required. --- ---- root_dir: {string} specifying the directory where the LSP server will base ---- as its rootUri on initialization. +--@param root_dir: (required, string) Directory where the LSP server will base +--- its rootUri on initialization. --- ---- cmd: {string} or {list} which is the base command to execute for the LSP. A ---- string will be run using 'shell' and a list will be interpreted as a bare ---- command with arguments passed. This is the same as |jobstart()|. +--@param cmd: (required, string or list treated like |jobstart()|) Base command +--- that initiates the LSP client. --- ---@param cmd_cwd: {string} specifying the directory to launch the `cmd` process. This ---- is not related to `root_dir`. By default, |getcwd()| is used. +--@param cmd_cwd: (string, default=|getcwd()|) Directory to launch +--- the `cmd` process. Not related to `root_dir`. --- ---@param cmd_env: {table} specifying the environment flags to pass to the LSP on ---- spawn. This can be specified using keys like a map or as a list with `k=v` ---- pairs or both. Non-string values are coerced to a string. ---- For example: `{ "PRODUCTION=true"; "TEST=123"; PORT = 8080; HOST = "0.0.0.0"; }`. +--@param cmd_env: (table) Environment flags to pass to the LSP on +--- spawn. Can be specified using keys like a map or as a list with `k=v` +--- pairs or both. Non-string values are coerced to string. +--- Example: +---
+--- { "PRODUCTION=true"; "TEST=123"; PORT = 8080; HOST = "0.0.0.0"; }
+--- 
--- ---@param capabilities: Map overriding the default capabilities defined by +--@param capabilities Map overriding the default capabilities defined by --- |vim.lsp.protocol.make_client_capabilities()|, passed to the language --- server on initialization. Hint: use make_client_capabilities() and modify --- its result. @@ -296,9 +297,8 @@ end --- `{[vim.type_idx]=vim.types.dictionary}`, else it will be encoded as an --- array. --- ---@param callbacks: Map of language server method names to ---- `function(err, method, params, client_id)` handler. ---- Invoked for: +--@param callbacks Map of language server method names to +--- `function(err, method, params, client_id)` handler. Invoked for: --- - Notifications from the server, where `err` will always be `nil`. --- - Requests initiated by the server. For these you can respond by returning --- two values: `result, err` where err must be shaped like a RPC error, @@ -307,32 +307,30 @@ end --- - Default callback for client requests not explicitly specifying --- a callback. --- ---@param init_options values to pass in the initialization request +--@param init_options Values to pass in the initialization request --- as `initializationOptions`. See `initialize` in the LSP spec. --- ---@param name: string used in log messages. Defaults to {client_id} +--@param name (string, default=client-id) Name in log messages. --- ---@param offset_encoding: One of "utf-8", "utf-16", or "utf-32" which is the ---- encoding that the LSP server expects. By default, it is "utf-16" as ---- specified in the LSP specification. The client does not verify this ---- is correct. +--@param offset_encoding (default="utf-16") One of "utf-8", "utf-16", +--- or "utf-32" which is the encoding that the LSP server expects. Client does +--- not verify this is correct. --- --@param on_error Callback with parameters (code, ...), invoked ---- when the client operation throws an error. ---- {code} is a number describing the error. Other arguments may be ---- passed depending on the error kind. See |vim.lsp.client_errors| for ---- possible errors. Use `vim.lsp.client_errors[code]` to get human-friendly ---- name. +--- when the client operation throws an error. `code` is a number describing +--- the error. Other arguments may be passed depending on the error kind. See +--- |vim.lsp.client_errors| for possible errors. +--- Use `vim.lsp.client_errors[code]` to get human-friendly name. --- ---@param before_init Callback with parameters (initialize_params, config) invoked ---- before the LSP "initialize" phase, where `params` contains the ---- parameters being sent to the server and `config` is the config ---- that was passed to `start_client()`. You can use this to modify ---- parameters before they are sent. +--@param before_init Callback with parameters (initialize_params, config) +--- invoked before the LSP "initialize" phase, where `params` contains the +--- parameters being sent to the server and `config` is the config that was +--- passed to `start_client()`. You can use this to modify parameters before +--- they are sent. --- --@param on_init Callback (client, initialize_result) invoked after LSP ---- "initialize", where `result` is a table of `capabilities` and ---- anything else the server may send. For example, clangd sends +--- "initialize", where `result` is a table of `capabilities` and anything else +--- the server may send. For example, clangd sends --- `initialize_result.offsetEncoding` if `capabilities.offsetEncoding` was --- sent to it. You can only modify the `client.offset_encoding` here before --- any notifications are sent. @@ -764,25 +762,30 @@ function lsp.get_client_by_id(client_id) return active_clients[client_id] end ---- Stops a client. +--- Stops a client(s). --- --- You can also use the `stop()` function on a |vim.lsp.client| object. +--- To stop all clients: +--- +---
+--- vim.lsp.stop_client(lsp.get_active_clients())
+--- 
--- --- By default asks the server to shutdown, unless stop was requested --- already for this client, then force-shutdown is attempted. --- ---@param client_id client id number +--@param client_id client id or |vim.lsp.client| object, or list thereof --@param force boolean (optional) shutdown forcefully function lsp.stop_client(client_id, force) - local client - client = active_clients[client_id] - if client then - client.stop(force) - return - end - client = uninitialized_clients[client_id] - if client then - client.stop(true) + local ids = type(client_id) == 'table' and client_id or {client_id} + for _, id in ipairs(ids) do + if type(id) == 'table' and id.stop ~= nil then + id.stop(force) + elseif active_clients[id] then + active_clients[id].stop(force) + elseif uninitialized_clients[id] then + uninitialized_clients[id].stop(true) + end end end @@ -793,18 +796,6 @@ function lsp.get_active_clients() return vim.tbl_values(active_clients) end ---- Stops all clients. ---- ---@param force boolean (optional) shutdown forcefully -function lsp.stop_all_clients(force) - for _, client in pairs(uninitialized_clients) do - client.stop(true) - end - for _, client in pairs(active_clients) do - client.stop(force) - end -end - function lsp._vim_exit_handler() log.info("exit_handler", active_clients) for _, client in pairs(uninitialized_clients) do -- cgit From ac6ebfcc1db8daf30533c42d5c4246bb95ec3d85 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Tue, 31 Dec 2019 08:03:06 -0800 Subject: LSP: eliminate lsp.print_debug_info…() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reduce API surface. We should not add functions unless they are really needed. Users should be nudged to use vim.inspect() directly. --- runtime/lua/vim/lsp.lua | 13 ------------- 1 file changed, 13 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 670de7a996..c193fad6a4 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -971,19 +971,6 @@ function lsp.buf_get_clients(bufnr) return result end ---- Prints debug info about the current buffer clients. ---- ---- Result of this function cannot be relied upon and may change. -function lsp.buf_print_debug_info(bufnr) - print(vim.inspect(lsp.buf_get_clients(bufnr))) -end - --- Print some debug information about all LSP related things. --- The output of this function should not be relied upon and may change. -function lsp.print_debug_info() - print(vim.inspect({ clients = active_clients })) -end - -- Log level dictionary with reverse lookup as well. -- -- Can be used to lookup the number from the name or the -- cgit From ea4127e9a7a624484f51c21e17f37c766da15da0 Mon Sep 17 00:00:00 2001 From: Björn Linse Date: Wed, 27 Nov 2019 20:45:41 +0100 Subject: lua: metatable for empty dict value --- runtime/lua/vim/inspect.lua | 5 +++++ runtime/lua/vim/shared.lua | 15 +++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/inspect.lua b/runtime/lua/vim/inspect.lua index 0f3b908dc1..0448ea487f 100644 --- a/runtime/lua/vim/inspect.lua +++ b/runtime/lua/vim/inspect.lua @@ -244,6 +244,11 @@ function Inspector:putTable(t) local nonSequentialKeys, nonSequentialKeysLength, sequenceLength = getNonSequentialKeys(t) local mt = getmetatable(t) + if (vim and sequenceLength == 0 and nonSequentialKeysLength == 0 + and mt == vim._empty_dict_mt) then + self:puts(tostring(t)) + return + end self:puts('{') self:down(function() diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index b5b04d7757..6df9bf1c2f 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -275,9 +275,15 @@ function vim.tbl_flatten(t) end -- Determine whether a Lua table can be treated as an array. +-- +-- An empty table `{}` will default to being treated as an array. +-- Use `vim.emtpy_dict()` to create a table treated as an +-- empty dict. Empty tables returned by `rpcrequest()` and +-- `vim.fn` functions can be checked using this function +-- whether they represent empty API arrays and vimL lists. --- --@params Table ---@returns true: A non-empty array, false: A non-empty table, nil: An empty table +--@returns true: An array-like table, false: A dict-like or mixed table function vim.tbl_islist(t) if type(t) ~= 'table' then return false @@ -296,7 +302,12 @@ function vim.tbl_islist(t) if count > 0 then return true else - return nil + -- TODO(bfredl): in the future, we will always be inside nvim + -- then this check can be deleted. + if vim._empty_dict_mt == nil then + return nil + end + return getmetatable(t) ~= vim._empty_dict_mt end end -- cgit From c241395b3dcc86cd1dd6358105d71cab524d6e50 Mon Sep 17 00:00:00 2001 From: Ville Hakulinen Date: Fri, 3 Jan 2020 14:39:25 +0200 Subject: LSP: place hover window by vertical space #11657 Make the hover window position itself vertically wherever is the most space available. --- runtime/lua/vim/lsp/util.lua | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 4a781359d4..0de5fdad46 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -264,11 +264,16 @@ function M.make_floating_popup_options(width, height, opts) local anchor = '' local row, col - if vim.fn.winline() <= height then + local lines_above = vim.fn.winline() - 1 + local lines_below = vim.fn.winheight(0) - lines_above + + if lines_above < lines_below then anchor = anchor..'N' + height = math.min(lines_below, height) row = 1 else anchor = anchor..'S' + height = math.min(lines_above, height) row = 0 end -- cgit From e616ec439436816bb39b1d5e720593e6c66580c9 Mon Sep 17 00:00:00 2001 From: Alvaro Muñoz Date: Fri, 3 Jan 2020 23:35:09 +0100 Subject: LSP: differentiate diagnostic underline by severity --- runtime/lua/vim/lsp/util.lua | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 0de5fdad46..338aedb4e0 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -547,7 +547,10 @@ do local diagnostic_ns = api.nvim_create_namespace("vim_lsp_diagnostics") local underline_highlight_name = "LspDiagnosticsUnderline" - api.nvim_command(string.format("highlight default %s gui=underline cterm=underline", underline_highlight_name)) + vim.cmd(string.format("highlight default %s gui=underline cterm=underline", underline_highlight_name)) + for kind, _ in pairs(protocol.DiagnosticSeverity) do + vim.cmd(string.format("highlight default link %s%s %s", underline_highlight_name, kind, underline_highlight_name)) + end local severity_highlights = {} @@ -657,13 +660,21 @@ do function M.buf_diagnostics_underline(bufnr, diagnostics) for _, diagnostic in ipairs(diagnostics) do - local start = diagnostic.range.start + local start = diagnostic.range["start"] local finish = diagnostic.range["end"] + local hlmap = { + [protocol.DiagnosticSeverity.Error]='Error', + [protocol.DiagnosticSeverity.Warning]='Warning', + [protocol.DiagnosticSeverity.Information]='Information', + [protocol.DiagnosticSeverity.Hint]='Hint', + } + -- TODO care about encoding here since this is in byte index? - highlight_range(bufnr, diagnostic_ns, underline_highlight_name, - {start.line, start.character}, - {finish.line, finish.character} + highlight_range(bufnr, diagnostic_ns, + underline_highlight_name..hlmap[diagnostic.severity], + {start.line, start.character}, + {finish.line, finish.character} ) end end -- cgit From 0a1c6d9a374a0c984515d0af43b1c71af6c55eb2 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Wed, 8 Jan 2020 09:46:25 -0800 Subject: LSP: highlight groups test, doc --- runtime/lua/vim/lsp/util.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 338aedb4e0..df82e2d412 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -549,7 +549,9 @@ do local underline_highlight_name = "LspDiagnosticsUnderline" vim.cmd(string.format("highlight default %s gui=underline cterm=underline", underline_highlight_name)) for kind, _ in pairs(protocol.DiagnosticSeverity) do - vim.cmd(string.format("highlight default link %s%s %s", underline_highlight_name, kind, underline_highlight_name)) + if type(kind) == 'string' then + vim.cmd(string.format("highlight default link %s%s %s", underline_highlight_name, kind, underline_highlight_name)) + end end local severity_highlights = {} -- cgit From 92316849863bb2661ee5b4bb284f56163fed27ad Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Sun, 12 Jan 2020 23:41:55 -0800 Subject: doc [ci skip] #11656 --- runtime/lua/vim/lsp.lua | 15 +++++++++++++-- runtime/lua/vim/shared.lua | 20 ++++++++++---------- 2 files changed, 23 insertions(+), 12 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index c193fad6a4..cfa208f21c 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -914,7 +914,18 @@ function lsp.buf_notify(bufnr, method, params) end) end ---- Function which can be called to generate omnifunc compatible completion. +--- Implements 'omnifunc' compatible LSP completion. +--- +--@see |complete-functions| +--@see |complete-items| +--@see |CompleteDone| +--- +--@param findstart 0 or 1, decides behavior +--@param base If findstart=0, text to match against +--- +--@return (number) Decided by `findstart`: +--- - findstart=0: column where the completion starts, or -2 or -3 +--- - findstart=1: list of matches (actually just calls |complete()|) function lsp.omnifunc(findstart, base) local _ = log.debug() and log.debug("omnifunc.findstart", { findstart = findstart, base = base }) @@ -936,7 +947,7 @@ function lsp.omnifunc(findstart, base) local line_to_cursor = line:sub(1, pos[2]) local _ = log.trace() and log.trace("omnifunc.line", pos, line) - -- Get the start postion of the current keyword + -- Get the start position of the current keyword local textMatch = vim.fn.match(line_to_cursor, '\\k*$') local params = util.make_position_params() diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 6df9bf1c2f..a71e9878bb 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -331,21 +331,21 @@ function vim.pesc(s) return s:gsub('[%(%)%.%%%+%-%*%?%[%]%^%$]', '%%%1') end ---- Test if `prefix` is a prefix of `s` for strings. --- --- @param s String to check --- @param prefix Potential prefix --- @return boolean True if prefix is a prefix of s +--- Tests if `s` starts with `prefix`. +--- +--@param s (string) a string +--@param prefix (string) a prefix +--@return (boolean) true if `prefix` is a prefix of s function vim.startswith(s, prefix) vim.validate { s = {s, 's'}; prefix = {prefix, 's'}; } return s:sub(1, #prefix) == prefix end ---- Test if `suffix` is a suffix of `s` for strings. --- --- @param s String to check --- @param suffix Potential suffix --- @return boolean True if suffix is a suffix of s +--- Tests if `s` ends with `suffix`. +--- +--@param s (string) a string +--@param suffix (string) a suffix +--@return (boolean) true if `suffix` is a suffix of s function vim.endswith(s, suffix) vim.validate { s = {s, 's'}; suffix = {suffix, 's'}; } return #suffix == 0 or s:sub(-#suffix) == suffix -- cgit From c6ff23d7a0d5ccf0d8995e3204c18df55d28fc7f Mon Sep 17 00:00:00 2001 From: Chris LaRose Date: Sun, 26 Jan 2020 00:24:42 -0800 Subject: terminal: absolute CWD in term:// URI #11289 This makes it possible to restore the working directory of :terminal buffers when reading those buffers from a session file. Fixes #11288 Co-authored-by: Justin M. Keyes --- runtime/lua/vim/shared.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index a71e9878bb..ea1117a906 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -321,7 +321,7 @@ function vim.trim(s) return s:match('^%s*(.*%S)') or '' end ---- Escapes magic chars in a Lua pattern string. +--- Escapes magic chars in a Lua pattern. --- --@see https://github.com/rxi/lume --@param s String to escape -- cgit From e956ea767241268861f0a8f7556700516849b112 Mon Sep 17 00:00:00 2001 From: Matthieu Coudron Date: Tue, 28 Jan 2020 10:45:25 +0100 Subject: LSP: show diagnostic in qf/loclist #11777 instead of the content of the file at this line. ref https://github.com/neovim/nvim-lsp/issues/69 --- runtime/lua/vim/lsp/util.lua | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index df82e2d412..6a1b73045e 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -704,7 +704,7 @@ do end local position_sort = sort_by_key(function(v) - return {v.line, v.character} + return {v.start.line, v.start.character} end) -- Returns the items with the byte position calculated correctly and in sorted @@ -721,17 +721,21 @@ function M.locations_to_items(locations) for _, d in ipairs(locations) do local start = d.range.start local fname = assert(vim.uri_to_fname(d.uri)) - table.insert(grouped[fname], start) + table.insert(grouped[fname], {start = start, msg= d.message }) end + + local keys = vim.tbl_keys(grouped) table.sort(keys) -- TODO(ashkan) I wish we could do this lazily. for _, fname in ipairs(keys) do local rows = grouped[fname] + table.sort(rows, position_sort) local i = 0 for line in io.lines(fname) do - for _, pos in ipairs(rows) do + for _, temp in ipairs(rows) do + local pos = temp.start local row = pos.line if i == row then local col @@ -744,7 +748,7 @@ function M.locations_to_items(locations) filename = fname, lnum = row + 1, col = col + 1; - text = line; + text = temp.msg; }) end end -- cgit From 00c57c98dfb2df58875a3d9ad8fc557ec9a24cba Mon Sep 17 00:00:00 2001 From: Björn Linse Date: Sat, 25 Jan 2020 13:43:41 +0100 Subject: treesitter: add standard &rtp/parser/ search path for parsers --- runtime/lua/vim/treesitter.lua | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua index aa8b8fcdd1..0d0e22adb3 100644 --- a/runtime/lua/vim/treesitter.lua +++ b/runtime/lua/vim/treesitter.lua @@ -31,8 +31,6 @@ function Parser:_on_lines(bufnr, _, start_row, old_stop_row, stop_row, old_byte_ end local M = { - add_language=vim._ts_add_language, - inspect_language=vim._ts_inspect_language, parse_query = vim._ts_parse_query, } @@ -45,12 +43,34 @@ setmetatable(M, { end }) -function M.create_parser(bufnr, ft, id) +function M.require_language(lang, path) + if vim._ts_has_language(lang) then + return true + end + if path == nil then + local fname = 'parser/' .. lang .. '.*' + local paths = a.nvim_get_runtime_file(fname, false) + if #paths == 0 then + -- TODO(bfredl): help tag? + error("no parser for '"..lang.."' language") + end + path = paths[1] + end + vim._ts_add_language(path, lang) +end + +function M.inspect_language(lang) + M.require_language(lang) + return vim._ts_inspect_language(lang) +end + +function M.create_parser(bufnr, lang, id) + M.require_language(lang) if bufnr == 0 then bufnr = a.nvim_get_current_buf() end - local self = setmetatable({bufnr=bufnr, lang=ft, valid=false}, Parser) - self._parser = vim._create_ts_parser(ft) + local self = setmetatable({bufnr=bufnr, lang=lang, valid=false}, Parser) + self._parser = vim._create_ts_parser(lang) self.change_cbs = {} self:parse() -- TODO(bfredl): use weakref to self, so that the parser is free'd is no plugin is @@ -94,6 +114,7 @@ local Query = {} Query.__index = Query function M.parse_query(lang, query) + M.require_language(lang) local self = setmetatable({}, Query) self.query = vim._ts_parse_query(lang, query) self.info = self.query:inspect() -- cgit From dd8b29cfe25604c062b76bb3a9347c5d740365ba Mon Sep 17 00:00:00 2001 From: Hirokazu Hata Date: Sun, 9 Feb 2020 15:51:02 +0900 Subject: LSP: set InitializeParams.rootPath value #11838 InitializeParams.rootPath is deprecated now. But some language servers still use it. --- runtime/lua/vim/lsp.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index cfa208f21c..304686a816 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -452,7 +452,7 @@ function lsp.start_client(config) -- The rootPath of the workspace. Is null if no folder is open. -- -- @deprecated in favour of rootUri. - rootPath = nil; + rootPath = config.root_dir; -- The rootUri of the workspace. Is null if no folder is open. If both -- `rootPath` and `rootUri` are set `rootUri` wins. rootUri = vim.uri_from_fname(config.root_dir); -- cgit From 174f7a29801be2d6680c8e7e5a3625c8c7dfe458 Mon Sep 17 00:00:00 2001 From: Mathias Fußenegger Date: Mon, 10 Feb 2020 01:05:42 +0100 Subject: lsp: Support text edit on inactive buffer (#11843) Using `vim.lsp.buf.rename()` can result in receiving a TextEdit that affects a file for which there is no active or loaded buffer. In that case `api.nvim_buf_get_lines(...)` returned an empty result, leading to an error. Closes https://github.com/neovim/neovim/issues/11790 --- runtime/lua/vim/lsp/util.lua | 3 +++ 1 file changed, 3 insertions(+) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 6a1b73045e..428874f2b7 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -88,6 +88,9 @@ function M.apply_text_edits(text_edits, bufnr) -- Reverse sort the orders so we can apply them without interfering with -- eachother. Also add i as a sort key to mimic a stable sort. table.sort(cleaned, edit_sort_key) + if not api.nvim_buf_is_loaded(bufnr) then + vim.fn.bufload(bufnr) + end local lines = api.nvim_buf_get_lines(bufnr, start_line, finish_line + 1, false) local fix_eol = api.nvim_buf_get_option(bufnr, 'fixeol') local set_eol = fix_eol and api.nvim_buf_line_count(bufnr) <= finish_line + 1 -- cgit From 95fd28f4a1ef4e298695e0204bdb5b347aa0f57c Mon Sep 17 00:00:00 2001 From: Björn Linse Date: Fri, 17 Jan 2020 22:27:17 +0100 Subject: treesitter: use internal "decorations" buffer --- runtime/lua/vim/tshighlighter.lua | 26 ++------------------------ 1 file changed, 2 insertions(+), 24 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/tshighlighter.lua b/runtime/lua/vim/tshighlighter.lua index 1544ecbf49..9d094f0f9a 100644 --- a/runtime/lua/vim/tshighlighter.lua +++ b/runtime/lua/vim/tshighlighter.lua @@ -85,7 +85,6 @@ end function TSHighlighter:on_window(_, _win, _buf, _topline, botline) self.iter = nil - self.active_nodes = {} self.nextrow = 0 self.botline = botline self.redraw_count = self.redraw_count + 1 @@ -107,34 +106,13 @@ function TSHighlighter:on_line(_, _win, buf, line) end local start_row, start_col, end_row, end_col = node:range() local hl = self.id_map[capture] - if hl > 0 and active then - if start_row == line and end_row == line then - a.nvim__put_attr(hl, start_col, end_col) - elseif end_row >= line then - -- TODO(bfredl): this is quite messy. Togheter with multiline bufhl we should support - -- luahl generating multiline highlights (and other kinds of annotations) - self.active_nodes[{hl=hl, start_row=start_row, start_col=start_col, end_row=end_row, end_col=end_col}] = true - end + if hl > 0 and active and end_row >= line then + a.nvim__put_attr(hl, start_row, start_col, end_row, end_col) end if start_row > line then self.nextrow = start_row end end - for node,_ in pairs(self.active_nodes) do - if node.start_row <= line and node.end_row >= line then - local start_col, end_col = node.start_col, node.end_col - if node.start_row < line then - start_col = 0 - end - if node.end_row > line then - end_col = 9000 - end - a.nvim__put_attr(node.hl, start_col, end_col) - end - if node.end_row <= line then - self.active_nodes[node] = nil - end - end self.line_count[line] = (self.line_count[line] or 0) + 1 --return tostring(self.line_count[line]) end -- cgit From 5d5b068d5bdd2fe3787eb619097a5af08ef929e2 Mon Sep 17 00:00:00 2001 From: Jesse-Bakker Date: Tue, 11 Feb 2020 07:53:14 +0100 Subject: LSP: Refine formatting tabSize #11834 Use the logic explained in the softtabstop help section for defining the tabSize parameter in formatting requests. This means that: - if softtabstop is 0, tabstop is used - if softtabstop < 0, shiftwidth is used - if softtabstop > 0, softtabstop is used When inserting spaces instead of tabs, softtabstop is used in vim. Therefor it would be more logical to use it when formatting instead of the current tabstop. --- runtime/lua/vim/lsp/buf.lua | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index a6a05fb095..19deb5df45 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -68,8 +68,9 @@ end function M.formatting(options) validate { options = {options, 't', true} } + local sts = vim.bo.softtabstop; options = vim.tbl_extend('keep', options or {}, { - tabSize = vim.bo.tabstop; + tabSize = (sts > 0 and sts) or (sts < 0 and vim.bo.shiftwidth) or vim.bo.tabstop; insertSpaces = vim.bo.expandtab; }) local params = { @@ -85,8 +86,9 @@ function M.range_formatting(options, start_pos, end_pos) start_pos = {start_pos, 't', true}; end_pos = {end_pos, 't', true}; } + local sts = vim.bo.softtabstop; options = vim.tbl_extend('keep', options or {}, { - tabSize = vim.bo.tabstop; + tabSize = (sts > 0 and sts) or (sts < 0 and vim.bo.shiftwidth) or vim.bo.tabstop; insertSpaces = vim.bo.expandtab; }) local A = list_extend({}, start_pos or api.nvim_buf_get_mark(0, '<')) -- cgit From 58ec72f9fdfd186e5154ce82be1da7c65b9c8ea0 Mon Sep 17 00:00:00 2001 From: Matthieu Coudron Date: Wed, 12 Feb 2020 06:48:25 +0100 Subject: LSP: rename validate_command to _cmd_parts #11847 and now only accepts a list of strings (instead of string or list). --- runtime/lua/vim/lsp.lua | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 304686a816..e5b6653346 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -121,13 +121,9 @@ local function validate_encoding(encoding) or error(string.format("Invalid offset encoding %q. Must be one of: 'utf-8', 'utf-16', 'utf-32'", encoding)) end -local function validate_command(input) +function lsp._cmd_parts(input) local cmd, cmd_args - if type(input) == 'string' then - -- Use a shell to execute the command if it is a string. - cmd = vim.api.nvim_get_option('shell') - cmd_args = {vim.api.nvim_get_option('shellcmdflag'), input} - elseif vim.tbl_islist(input) then + if vim.tbl_islist(input) then cmd = input[1] cmd_args = {} -- Don't mutate our input. @@ -138,7 +134,7 @@ local function validate_command(input) end end else - error("cmd type must be string or list.") + error("cmd type must be list.") end return cmd, cmd_args end @@ -166,7 +162,7 @@ local function validate_client_config(config) before_init = { config.before_init, "f", true }; offset_encoding = { config.offset_encoding, "s", true }; } - local cmd, cmd_args = validate_command(config.cmd) + local cmd, cmd_args = lsp._cmd_parts(config.cmd) local offset_encoding = valid_encodings.UTF16 if config.offset_encoding then offset_encoding = validate_encoding(config.offset_encoding) -- cgit From 417fc6ccf78801aef79a8731c5a85db6b12cd407 Mon Sep 17 00:00:00 2001 From: Hirokazu Hata Date: Thu, 13 Feb 2020 11:55:43 +0900 Subject: lua: vim.deepcopy uses empty_dict() instead of {} for empty_dict() fix: https://github.com/neovim/nvim-lsp/issues/94 --- runtime/lua/vim/shared.lua | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index ea1117a906..36df24d0c1 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -20,6 +20,11 @@ vim.deepcopy = (function() local deepcopy_funcs = { table = function(orig) local copy = {} + + if getmetatable(orig) == vim._empty_dict_mt then + copy = vim.empty_dict() + end + for k, v in pairs(orig) do copy[vim.deepcopy(k)] = vim.deepcopy(v) end -- cgit From c230c7d1a6b744efb673f516f0c6cc2a17c2305b Mon Sep 17 00:00:00 2001 From: Hirokazu Hata Date: Thu, 13 Feb 2020 15:02:30 +0900 Subject: lua: if second argument is vim.empty_dict(), vim.tbl_extend uses empty_dict() instead of {} --- runtime/lua/vim/shared.lua | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 36df24d0c1..6eb7a970e4 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -21,7 +21,7 @@ vim.deepcopy = (function() table = function(orig) local copy = {} - if getmetatable(orig) == vim._empty_dict_mt then + if vim._empty_dict_mt ~= nil and getmetatable(orig) == vim._empty_dict_mt then copy = vim.empty_dict() end @@ -174,9 +174,19 @@ function vim.tbl_extend(behavior, ...) if (behavior ~= 'error' and behavior ~= 'keep' and behavior ~= 'force') then error('invalid "behavior": '..tostring(behavior)) end + + if select('#', ...) < 2 then + error('wrong number of arguments (given '..tostring(1 + select('#', ...))..', expected at least 3)') + end + local ret = {} + if vim._empty_dict_mt ~= nil and getmetatable(select(1, ...)) == vim._empty_dict_mt then + ret = vim.empty_dict() + end + for i = 1, select('#', ...) do local tbl = select(i, ...) + vim.validate{["after the second argument"] = {tbl,'t'}} if tbl then for k, v in pairs(tbl) do if behavior ~= 'force' and ret[k] ~= nil then -- cgit From 1eb0f5371ae8cee90b97f586a99505cfa5913504 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Sat, 8 Feb 2020 17:25:53 -0800 Subject: LSP: fix validate_client_config - `cmd_env` is a table not a function. - tests: Set $NVIM_LOG_FILE for fake LSP server. --- runtime/lua/vim/lsp.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index e5b6653346..94f0d62d8d 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -154,7 +154,7 @@ local function validate_client_config(config) callbacks = { config.callbacks, "t", true }; capabilities = { config.capabilities, "t", true }; cmd_cwd = { config.cmd_cwd, optional_validator(is_dir), "directory" }; - cmd_env = { config.cmd_env, "f", true }; + cmd_env = { config.cmd_env, "t", true }; name = { config.name, 's', true }; on_error = { config.on_error, "f", true }; on_exit = { config.on_exit, "f", true }; -- cgit From a446fbc8fa278d9c1e4144dc9767c9dc0b184583 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Sun, 9 Feb 2020 00:05:48 -0800 Subject: lsp/rpc.lua: fix `env` application function Env vars must be merged with the current env. --- runtime/lua/vim/lsp/rpc.lua | 51 +++++++++++++++++---------------------------- 1 file changed, 19 insertions(+), 32 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua index e13b05610b..74d73da31f 100644 --- a/runtime/lua/vim/lsp/rpc.lua +++ b/runtime/lua/vim/lsp/rpc.lua @@ -33,38 +33,25 @@ local function convert_NIL(v) return v end --- If a dictionary is passed in, turn it into a list of string of "k=v" --- Accepts a table which can be composed of k=v strings or map-like --- specification, such as: --- --- ``` --- { --- "PRODUCTION=false"; --- "PATH=/usr/bin/"; --- PORT = 123; --- HOST = "0.0.0.0"; --- } --- ``` --- --- Non-string values will be cast with `tostring` -local function force_env_list(final_env) - if final_env then - local env = final_env - final_env = {} - for k,v in pairs(env) do - -- If it's passed in as a dict, then convert to list of "k=v" - if type(k) == "string" then - table.insert(final_env, k..'='..tostring(v)) - elseif type(v) == 'string' then - table.insert(final_env, v) - else - -- TODO is this right or should I exception here? - -- Try to coerce other values to string. - table.insert(final_env, tostring(v)) - end - end - return final_env +--- Merges current process env with the given env and returns the result as +--- a list of "k=v" strings. +--- +--- Example: +--- +--- { PRODUCTION="false", PATH="/usr/bin/", PORT=123, HOST="0.0.0.0", } +--- => { "PRODUCTION=false", "PATH=/usr/bin/", "PORT=123", "HOST=0.0.0.0", } +local function env_merge(env) + if env == nil then + return env + end + -- Merge. + env = vim.tbl_extend('force', vim.fn.environ(), env) + local final_env = {} + for k,v in pairs(env) do + assert(type(k) == 'string', 'env must be a dict') + table.insert(final_env, k..'='..tostring(v)) end + return final_env end local function format_message_with_content_length(encoded_message) @@ -262,7 +249,7 @@ local function create_and_start_client(cmd, cmd_args, handlers, extra_spawn_para if spawn_params.cwd then assert(is_dir(spawn_params.cwd), "cwd must be a directory") end - spawn_params.env = force_env_list(extra_spawn_params.env) + spawn_params.env = env_merge(extra_spawn_params.env) end handle, pid = uv.spawn(cmd, spawn_params, onexit) end -- cgit From e2ed8053bf722d4d111fac7dcdb07179fdea8752 Mon Sep 17 00:00:00 2001 From: Hirokazu Hata Date: Tue, 18 Feb 2020 17:41:29 +0900 Subject: lua: move test helper function, map and filter, to vim.shared module --- runtime/lua/vim/shared.lua | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 6eb7a970e4..498992aa2e 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -135,6 +135,36 @@ function vim.tbl_values(t) return values end +--- Apply a function to all values of a table. +--- +--@param func function or callable table +--@param t table +function vim.tbl_map(func, t) + vim.validate{func={func,'c'},t={t,'t'}} + + local rettab = {} + for k, v in pairs(t) do + rettab[k] = func(v) + end + return rettab +end + +--- Filter a table using a predicate function +--- +--@param func function or callable table +--@param t table +function vim.tbl_filter(func, t) + vim.validate{func={func,'c'},t={t,'t'}} + + local rettab = {} + for _, entry in pairs(t) do + if func(entry) then + table.insert(rettab, entry) + end + end + return rettab +end + --- Checks if a list-like (vector) table contains `value`. --- --@param t Table to check -- cgit From 4ac376740c85ee337fc10627a793452300801ce0 Mon Sep 17 00:00:00 2001 From: Hirokazu Hata Date: Tue, 18 Feb 2020 13:38:52 +0900 Subject: lsp: fix textDocument/completion handling fix: #11826 Some lanuguage servers return complementary candidates whose prefixes do not match are also returned. So we exclude completion candidates whose prefix does not match. ex) Microsoft python-language-server, rust-analyzer --- runtime/lua/vim/lsp.lua | 4 +++- runtime/lua/vim/lsp/callbacks.lua | 3 ++- runtime/lua/vim/lsp/util.lua | 17 ++++++++++++++++- 3 files changed, 21 insertions(+), 3 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 94f0d62d8d..bc0da25ae5 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -945,12 +945,14 @@ function lsp.omnifunc(findstart, base) -- Get the start position of the current keyword local textMatch = vim.fn.match(line_to_cursor, '\\k*$') + local prefix = line_to_cursor:sub(textMatch+1) + local params = util.make_position_params() local items = {} lsp.buf_request(bufnr, 'textDocument/completion', params, function(err, _, result) if err or not result then return end - local matches = util.text_document_completion_list_to_complete_items(result) + local matches = util.text_document_completion_list_to_complete_items(result, prefix) -- TODO(ashkan): is this the best way to do this? vim.list_extend(items, matches) vim.fn.complete(textMatch+1, items) diff --git a/runtime/lua/vim/lsp/callbacks.lua b/runtime/lua/vim/lsp/callbacks.lua index 794140ee2e..e76e07ca96 100644 --- a/runtime/lua/vim/lsp/callbacks.lua +++ b/runtime/lua/vim/lsp/callbacks.lua @@ -63,8 +63,9 @@ M['textDocument/completion'] = function(_, _, result) local line = assert(api.nvim_buf_get_lines(0, row-1, row, false)[1]) local line_to_cursor = line:sub(col+1) local textMatch = vim.fn.match(line_to_cursor, '\\k*$') + local prefix = line_to_cursor:sub(textMatch+1) - local matches = util.text_document_completion_list_to_complete_items(result) + local matches = util.text_document_completion_list_to_complete_items(result, prefix) vim.fn.complete(textMatch+1, matches) end diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 428874f2b7..620037d24d 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -129,6 +129,19 @@ function M.extract_completion_items(result) end end +-- Some lanuguage servers return complementary candidates whose prefixes do not match are also returned. +-- So we exclude completion candidates whose prefix does not match. +function M.remove_unmatch_completion_items(items, prefix) + local matched_items = {} + for _, item in ipairs(items) do + local word = item.insertText or item.label + if vim.startswith(word, prefix) then + table.insert(matched_items, item) + end + end + return matched_items +end + --- Apply the TextDocumentEdit response. -- @params TextDocumentEdit [table] see https://microsoft.github.io/language-server-protocol/specification function M.apply_text_document_edit(text_document_edit) @@ -151,12 +164,14 @@ end --- Getting vim complete-items with incomplete flag. -- @params CompletionItem[], CompletionList or nil (https://microsoft.github.io/language-server-protocol/specification#textDocument_completion) -- @return { matches = complete-items table, incomplete = boolean } -function M.text_document_completion_list_to_complete_items(result) +function M.text_document_completion_list_to_complete_items(result, prefix) local items = M.extract_completion_items(result) if vim.tbl_isempty(items) then return {} end + items = M.remove_unmatch_completion_items(items, prefix) + local matches = {} for _, completion_item in ipairs(items) do -- cgit From c1bfc8093f4fb4487a2293f09558f46c7de49315 Mon Sep 17 00:00:00 2001 From: Hirokazu Hata Date: Tue, 18 Feb 2020 13:40:24 +0900 Subject: lsp: respect the sort order if there is sortText --- runtime/lua/vim/lsp/util.lua | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 620037d24d..0a24444328 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -129,6 +129,15 @@ function M.extract_completion_items(result) end end +-- Sort by CompletionItem.sortText +-- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion +function M.sort_completion_items(items) + if items[1] and items[1].sortText then + table.sort(items, function(a, b) return a.sortText < b.sortText + end) + end +end + -- Some lanuguage servers return complementary candidates whose prefixes do not match are also returned. -- So we exclude completion candidates whose prefix does not match. function M.remove_unmatch_completion_items(items, prefix) @@ -171,6 +180,7 @@ function M.text_document_completion_list_to_complete_items(result, prefix) end items = M.remove_unmatch_completion_items(items, prefix) + M.sort_completion_items(items) local matches = {} -- cgit From f3d4ddd0f8b654d58fb4653d88ac7f652e3ad364 Mon Sep 17 00:00:00 2001 From: Hirokazu Hata Date: Wed, 19 Feb 2020 07:39:56 +0900 Subject: lsp: make functions private and use filter function --- runtime/lua/vim/lsp/util.lua | 44 ++++++++++++++++++++------------------------ 1 file changed, 20 insertions(+), 24 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 0a24444328..6b12b37ec2 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -129,28 +129,6 @@ function M.extract_completion_items(result) end end --- Sort by CompletionItem.sortText --- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion -function M.sort_completion_items(items) - if items[1] and items[1].sortText then - table.sort(items, function(a, b) return a.sortText < b.sortText - end) - end -end - --- Some lanuguage servers return complementary candidates whose prefixes do not match are also returned. --- So we exclude completion candidates whose prefix does not match. -function M.remove_unmatch_completion_items(items, prefix) - local matched_items = {} - for _, item in ipairs(items) do - local word = item.insertText or item.label - if vim.startswith(word, prefix) then - table.insert(matched_items, item) - end - end - return matched_items -end - --- Apply the TextDocumentEdit response. -- @params TextDocumentEdit [table] see https://microsoft.github.io/language-server-protocol/specification function M.apply_text_document_edit(text_document_edit) @@ -170,6 +148,24 @@ function M.get_current_line_to_cursor() return line:sub(pos[2]+1) end +-- Sort by CompletionItem.sortText +-- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion +local function sort_completion_items(items) + if items[1] and items[1].sortText then + table.sort(items, function(a, b) return a.sortText < b.sortText + end) + end +end + +-- Some lanuguage servers return complementary candidates whose prefixes do not match are also returned. +-- So we exclude completion candidates whose prefix does not match. +local function remove_unmatch_completion_items(items, prefix) + return vim.tbl_filter(function(item) + local word = item.insertText or item.label + return vim.startswith(word, prefix) + end, items) +end + --- Getting vim complete-items with incomplete flag. -- @params CompletionItem[], CompletionList or nil (https://microsoft.github.io/language-server-protocol/specification#textDocument_completion) -- @return { matches = complete-items table, incomplete = boolean } @@ -179,8 +175,8 @@ function M.text_document_completion_list_to_complete_items(result, prefix) return {} end - items = M.remove_unmatch_completion_items(items, prefix) - M.sort_completion_items(items) + items = remove_unmatch_completion_items(items, prefix) + sort_completion_items(items) local matches = {} -- cgit From ff1730373c6139db14b8f2f9b24d4ccd7fcfb01d Mon Sep 17 00:00:00 2001 From: Mathias Fußenegger Date: Fri, 21 Feb 2020 09:34:07 +0100 Subject: lsp/completion: show duplicates in completion popup #11920 Allow duplicates so that in languages with overloaded functions it will show all signatures. E.g. instead of having a single (last one wins) add(int index, String element) It shows all signatures: add(String e) : boolean add(int index, String element) : void --- runtime/lua/vim/lsp/util.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 6b12b37ec2..b7c7b7f75d 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -201,7 +201,7 @@ function M.text_document_completion_list_to_complete_items(result, prefix) menu = completion_item.detail or '', info = info, icase = 1, - dup = 0, + dup = 1, empty = 1, }) end -- cgit From 08af82b9cbd74016b96db0d133f8f85aa2960d0b Mon Sep 17 00:00:00 2001 From: Björn Linse Date: Sat, 22 Feb 2020 19:02:08 +0100 Subject: treesitter: redraw on changed query --- runtime/lua/vim/tshighlighter.lua | 2 ++ 1 file changed, 2 insertions(+) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/tshighlighter.lua b/runtime/lua/vim/tshighlighter.lua index 9d094f0f9a..1440acf0d0 100644 --- a/runtime/lua/vim/tshighlighter.lua +++ b/runtime/lua/vim/tshighlighter.lua @@ -69,6 +69,8 @@ function TSHighlighter:set_query(query) end self.id_map[i] = hl end + + a.nvim__buf_redraw_range(self.buf, 0, a.nvim_buf_line_count(self.buf)) end function TSHighlighter:on_change(changes) -- cgit From 9c00fea585ccab56a6044a174ce8d9a2c605c6cd Mon Sep 17 00:00:00 2001 From: Björn Linse Date: Mon, 4 Nov 2019 20:40:30 +0100 Subject: lua: add regex support, and `@match` support in treesitter queries --- runtime/lua/vim/treesitter.lua | 46 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 6 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua index 0d0e22adb3..8dacfa11cf 100644 --- a/runtime/lua/vim/treesitter.lua +++ b/runtime/lua/vim/treesitter.lua @@ -113,12 +113,33 @@ end local Query = {} Query.__index = Query +local magic_prefixes = {['\\v']=true, ['\\m']=true, ['\\M']=true, ['\\V']=true} +local function check_magic(str) + if string.len(str) < 2 or magic_prefixes[string.sub(str,1,2)] then + return str + end + return '\\v'..str +end + function M.parse_query(lang, query) M.require_language(lang) local self = setmetatable({}, Query) self.query = vim._ts_parse_query(lang, query) self.info = self.query:inspect() self.captures = self.info.captures + self.regexes = {} + for id,preds in pairs(self.info.patterns) do + local regexes = {} + for i, pred in ipairs(preds) do + if (pred[1] == "match?" and type(pred[2]) == "number" + and type(pred[3]) == "string") then + regexes[i] = vim.regex(check_magic(pred[3])) + end + end + if next(regexes) then + self.regexes[id] = regexes + end + end return self end @@ -131,8 +152,13 @@ local function get_node_text(node, bufnr) return string.sub(line, start_col+1, end_col) end -local function match_preds(match, preds, bufnr) - for _, pred in pairs(preds) do +function Query:match_preds(match, pattern, bufnr) + local preds = self.info.patterns[pattern] + if not preds then + return true + end + local regexes = self.regexes[pattern] + for i, pred in pairs(preds) do if pred[1] == "eq?" then local node = match[pred[2]] local node_text = get_node_text(node, bufnr) @@ -149,6 +175,16 @@ local function match_preds(match, preds, bufnr) if node_text ~= str or str == nil then return false end + elseif pred[1] == "match?" then + if not regexes or not regexes[i] then + return false + end + local node = match[pred[2]] + local start_row, start_col, end_row, end_col = node:range() + if start_row ~= end_row then + return false + end + return regexes[i]:match_line(bufnr, start_row, start_col, end_col) else return false end @@ -164,8 +200,7 @@ function Query:iter_captures(node, bufnr, start, stop) local function iter() local capture, captured_node, match = raw_iter() if match ~= nil then - local preds = self.info.patterns[match.pattern] - local active = match_preds(match, preds, bufnr) + local active = self:match_preds(match, match.pattern, bufnr) match.active = active if not active then return iter() -- tail call: try next match @@ -184,8 +219,7 @@ function Query:iter_matches(node, bufnr, start, stop) local function iter() local pattern, match = raw_iter() if match ~= nil then - local preds = self.info.patterns[pattern] - local active = (not preds) or match_preds(match, preds, bufnr) + local active = self:match_preds(match, pattern, bufnr) if not active then return iter() -- tail call: try next match end -- cgit From ca8699378c765017575c102f3da8347833159a6c Mon Sep 17 00:00:00 2001 From: Alvaro Muñoz Date: Wed, 26 Feb 2020 20:10:16 +0100 Subject: LSP: implement documentHighlight (#11638) * implement documentHighlight * fix bug * document highlight groups * use uppercase for help section title * documentation --- runtime/lua/vim/lsp/buf.lua | 18 ++++++++++++++++++ runtime/lua/vim/lsp/callbacks.lua | 6 ++++++ runtime/lua/vim/lsp/util.lua | 22 ++++++++++++++++++++-- 3 files changed, 44 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 19deb5df45..52fa2ec93b 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -134,5 +134,23 @@ function M.references(context) request('textDocument/references', params) end +--- Send request to server to resolve document highlights for the +--- current text document position. This request can be associated +--- to key mapping or to events such as `CursorHold`, eg: +--- +---
+--- vim.api.nvim_command [[autocmd CursorHold   lua vim.lsp.buf.document_highlight()]]
+--- vim.api.nvim_command [[autocmd CursorHoldI  lua vim.lsp.buf.document_highlight()]]
+--- vim.api.nvim_command [[autocmd CursorMoved  lua vim.lsp.buf.clear_references()]]
+--- 
+function M.document_highlight() + local params = util.make_position_params() + request('textDocument/documentHighlight', params) +end + +function M.clear_references() + util.buf_clear_references() +end + return M -- vim:sw=2 ts=2 et diff --git a/runtime/lua/vim/lsp/callbacks.lua b/runtime/lua/vim/lsp/callbacks.lua index e76e07ca96..d7d74862b6 100644 --- a/runtime/lua/vim/lsp/callbacks.lua +++ b/runtime/lua/vim/lsp/callbacks.lua @@ -196,6 +196,12 @@ M['textDocument/peekDefinition'] = function(_, _, result, _) api.nvim_buf_add_highlight(headbuf, -1, 'Keyword', 0, -1) end +M['textDocument/documentHighlight'] = function(_, _, result, _) + if not result then return end + local bufnr = api.nvim_get_current_buf() + util.buf_highlight_references(bufnr, result) +end + local function log_message(_, _, result, client_id) local message_type = result.type local message = result.message diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index b7c7b7f75d..9c67f62e21 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -569,6 +569,7 @@ do local all_buffer_diagnostics = {} local diagnostic_ns = api.nvim_create_namespace("vim_lsp_diagnostics") + local reference_ns = api.nvim_create_namespace("vim_lsp_references") local underline_highlight_name = "LspDiagnosticsUnderline" vim.cmd(string.format("highlight default %s gui=underline cterm=underline", underline_highlight_name)) @@ -602,7 +603,6 @@ do function M.buf_clear_diagnostics(bufnr) validate { bufnr = {bufnr, 'n', true} } - bufnr = bufnr == 0 and api.nvim_get_current_buf() or bufnr api.nvim_buf_clear_namespace(bufnr, diagnostic_ns, 0, -1) end @@ -683,7 +683,6 @@ do end end - function M.buf_diagnostics_underline(bufnr, diagnostics) for _, diagnostic in ipairs(diagnostics) do local start = diagnostic.range["start"] @@ -705,6 +704,25 @@ do end end + function M.buf_clear_references(bufnr) + validate { bufnr = {bufnr, 'n', true} } + api.nvim_buf_clear_namespace(bufnr, reference_ns, 0, -1) + end + + function M.buf_highlight_references(bufnr, references) + validate { bufnr = {bufnr, 'n', true} } + for _, reference in ipairs(references) do + local start_pos = {reference["range"]["start"]["line"], reference["range"]["start"]["character"]} + local end_pos = {reference["range"]["end"]["line"], reference["range"]["end"]["character"]} + local document_highlight_kind = { + [protocol.DocumentHighlightKind.Text] = "LspReferenceText"; + [protocol.DocumentHighlightKind.Read] = "LspReferenceRead"; + [protocol.DocumentHighlightKind.Write] = "LspReferenceWrite"; + } + highlight_range(bufnr, reference_ns, document_highlight_kind[reference["kind"]], start_pos, end_pos) + end + end + function M.buf_diagnostics_virtual_text(bufnr, diagnostics) local buffer_line_diagnostics = all_buffer_diagnostics[bufnr] if not buffer_line_diagnostics then -- cgit From ad745f9da289a56035b8c759cc8fac1b40a44558 Mon Sep 17 00:00:00 2001 From: Alvaro Muñoz Date: Wed, 26 Feb 2020 20:22:14 +0100 Subject: add support to show diagnostics count in statusline (#11641) * add support to show diagnostics count in statusline * documentation --- runtime/lua/vim/lsp.lua | 17 +++++++++-------- runtime/lua/vim/lsp/buf.lua | 4 ++++ runtime/lua/vim/lsp/callbacks.lua | 1 + runtime/lua/vim/lsp/util.lua | 12 ++++++++++++ 4 files changed, 26 insertions(+), 8 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index bc0da25ae5..71ec3cb6c4 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -893,21 +893,22 @@ function lsp.buf_request_sync(bufnr, method, params, timeout_ms) return request_results end ---- Sends a notification to all servers attached to the buffer. ---- ---@param bufnr (optional, number) Buffer handle, or 0 for current ---@param method (string) LSP method name ---@param params (string) Parameters to send to the server ---- ---@returns nil +--- Send a notification to a server +-- @param bufnr [number] (optional): The number of the buffer +-- @param method [string]: Name of the request method +-- @param params [string]: Arguments to send to the server +-- +-- @returns true if any client returns true; false otherwise function lsp.buf_notify(bufnr, method, params) validate { bufnr = { bufnr, 'n', true }; method = { method, 's' }; } + local resp = false for_each_buffer_client(bufnr, function(client, _client_id) - client.rpc.notify(method, params) + if client.rpc.notify(method, params) then resp = true end end) + return resp end --- Implements 'omnifunc' compatible LSP completion. diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 52fa2ec93b..82aeccd4db 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -23,6 +23,10 @@ local function request(method, params, callback) return vim.lsp.buf_request(0, method, params, callback) end +function M.server_ready() + return not not vim.lsp.buf_notify(0, "window/progress", {}) +end + function M.hover() local params = util.make_position_params() request('textDocument/hover', params) diff --git a/runtime/lua/vim/lsp/callbacks.lua b/runtime/lua/vim/lsp/callbacks.lua index d7d74862b6..457eccb985 100644 --- a/runtime/lua/vim/lsp/callbacks.lua +++ b/runtime/lua/vim/lsp/callbacks.lua @@ -33,6 +33,7 @@ M['textDocument/publishDiagnostics'] = function(_, _, result) util.buf_diagnostics_underline(bufnr, result.diagnostics) util.buf_diagnostics_virtual_text(bufnr, result.diagnostics) -- util.set_loclist(result.diagnostics) + vim.api.nvim_command("doautocmd User LspDiagnosticsChanged") end M['textDocument/references'] = function(_, _, result) diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 9c67f62e21..59300647b5 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -743,6 +743,18 @@ do api.nvim_buf_set_virtual_text(bufnr, diagnostic_ns, line, virt_texts, {}) end end + function M.buf_diagnostics_count(kind) + local bufnr = vim.api.nvim_get_current_buf() + local buffer_line_diagnostics = all_buffer_diagnostics[bufnr] + if not buffer_line_diagnostics then return end + local count = 0 + for _, line_diags in pairs(buffer_line_diagnostics) do + for _, diag in ipairs(line_diags) do + if protocol.DiagnosticSeverity[kind] == diag.severity then count = count + 1 end + end + end + return count + end end local position_sort = sort_by_key(function(v) -- cgit From cb8b9428ca796c68025afbba65a81381b2f6a304 Mon Sep 17 00:00:00 2001 From: Mathias Fußenegger Date: Thu, 27 Feb 2020 00:00:06 +0100 Subject: LSP/hover: Do not throw away contents if first line is empty (#11939) haskell-ide-engine sends `hover` payloads as follows: { contents = { kind = "markdown", value = "\n```haskell\nfoo :: Either String (Integer, Text)\n```\n`foo` *local*" }, range = { end = { character = 5, line = 57 }, start = { character = 2, line = 57 } } } `value` starts with `\n`. The logic in `convert_input_to_markdown_lines` threw away the whole information. --- runtime/lua/vim/lsp/util.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 59300647b5..3eadefe051 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -269,7 +269,7 @@ function M.convert_input_to_markdown_lines(input, contents) end end end - if contents[1] == '' or contents[1] == nil then + if (contents[1] == '' or contents[1] == nil) and #contents == 1 then return {} end return contents -- cgit From 83add102cff8ac2ce2dda02cf1ac9225079f6529 Mon Sep 17 00:00:00 2001 From: Alvaro Muñoz Date: Thu, 27 Feb 2020 12:12:53 +0100 Subject: Add signs for Lsp diagnostics (#11668) * Add signs for Lsp diagnostics * defer sign definition until init.vim is loaded --- runtime/lua/vim/lsp/callbacks.lua | 1 + runtime/lua/vim/lsp/util.lua | 24 +++++++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/callbacks.lua b/runtime/lua/vim/lsp/callbacks.lua index 457eccb985..c9d63625fd 100644 --- a/runtime/lua/vim/lsp/callbacks.lua +++ b/runtime/lua/vim/lsp/callbacks.lua @@ -32,6 +32,7 @@ M['textDocument/publishDiagnostics'] = function(_, _, result) util.buf_diagnostics_save_positions(bufnr, result.diagnostics) util.buf_diagnostics_underline(bufnr, result.diagnostics) util.buf_diagnostics_virtual_text(bufnr, result.diagnostics) + util.buf_diagnostics_signs(bufnr, result.diagnostics) -- util.set_loclist(result.diagnostics) vim.api.nvim_command("doautocmd User LspDiagnosticsChanged") end diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 3eadefe051..21e0dbfd1f 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -570,7 +570,7 @@ do local diagnostic_ns = api.nvim_create_namespace("vim_lsp_diagnostics") local reference_ns = api.nvim_create_namespace("vim_lsp_references") - + local sign_ns = 'vim_lsp_signs' local underline_highlight_name = "LspDiagnosticsUnderline" vim.cmd(string.format("highlight default %s gui=underline cterm=underline", underline_highlight_name)) for kind, _ in pairs(protocol.DiagnosticSeverity) do @@ -603,6 +603,12 @@ do function M.buf_clear_diagnostics(bufnr) validate { bufnr = {bufnr, 'n', true} } + bufnr = bufnr == 0 and api.nvim_get_current_buf() or bufnr + + -- clear sign group + vim.fn.sign_unplace(sign_ns, {buffer=bufnr}) + + -- clear virtual text namespace api.nvim_buf_clear_namespace(bufnr, diagnostic_ns, 0, -1) end @@ -755,6 +761,22 @@ do end return count end + function M.buf_diagnostics_signs(bufnr, diagnostics) + vim.fn.sign_define('LspDiagnosticsErrorSign', {text=vim.g['LspDiagnosticsErrorSign'] or 'E', texthl='LspDiagnosticsError', linehl='', numhl=''}) + vim.fn.sign_define('LspDiagnosticsWarningSign', {text=vim.g['LspDiagnosticsWarningSign'] or 'W', texthl='LspDiagnosticsWarning', linehl='', numhl=''}) + vim.fn.sign_define('LspDiagnosticsInformationSign', {text=vim.g['LspDiagnosticsInformationSign'] or 'I', texthl='LspDiagnosticsInformation', linehl='', numhl=''}) + vim.fn.sign_define('LspDiagnosticsHintSign', {text=vim.g['LspDiagnosticsHintSign'] or 'H', texthl='LspDiagnosticsHint', linehl='', numhl=''}) + + for _, diagnostic in ipairs(diagnostics) do + local diagnostic_severity_map = { + [protocol.DiagnosticSeverity.Error] = "LspDiagnosticsErrorSign"; + [protocol.DiagnosticSeverity.Warning] = "LspDiagnosticsWarningSign"; + [protocol.DiagnosticSeverity.Information] = "LspDiagnosticsInformationSign"; + [protocol.DiagnosticSeverity.Hint] = "LspDiagnosticsHintSign"; + } + vim.fn.sign_place(0, sign_ns, diagnostic_severity_map[diagnostic.severity], bufnr, {lnum=(diagnostic.range.start.line+1)}) + end + end end local position_sort = sort_by_key(function(v) -- cgit From 220a2b05c6c42dcbaa0c84292cbb318ddee4ff49 Mon Sep 17 00:00:00 2001 From: Mathias Fussenegger Date: Wed, 19 Feb 2020 20:24:56 +0100 Subject: LSP/references: Add context to locations returned by server This changes the `textDocument/references' callback to annotate the locations returned by the server with the content present at the locations range. The quickfix list then looks as follows: cr8/insert_fake_data.py|17 col 32| from .misc import parse_table, parse_version cr8/insert_fake_data.py|43 col 15| version = parse_version(r['rows'][0][0]) cr8/java_magic.py|8 col 22| from cr8.misc import parse_version cr8/java_magic.py|30 col 19| version = parse_version(fst) cr8/java_magic.py|33 col 16| return parse_version(version_str) Instead of: cr8/insert_fake_data.py|17 col 32| cr8/insert_fake_data.py|43 col 15| cr8/java_magic.py|8 col 22| cr8/java_magic.py|30 col 19| cr8/java_magic.py|33 col 16| --- runtime/lua/vim/lsp/util.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 21e0dbfd1f..803d9443b7 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -824,7 +824,7 @@ function M.locations_to_items(locations) filename = fname, lnum = row + 1, col = col + 1; - text = temp.msg; + text = temp.msg or line; }) end end -- cgit From 38201650cd60c001c4b639f30ca45f1b8eba98ca Mon Sep 17 00:00:00 2001 From: Mathias Fussenegger Date: Sun, 23 Feb 2020 13:00:48 +0100 Subject: LSP: Remove diagnostic message handling in locations_to_items `locations_to_items` is for turning `Location[]` into items, not for `Diagnostic[]` --- runtime/lua/vim/lsp/callbacks.lua | 1 - runtime/lua/vim/lsp/util.lua | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/callbacks.lua b/runtime/lua/vim/lsp/callbacks.lua index c9d63625fd..99093216d9 100644 --- a/runtime/lua/vim/lsp/callbacks.lua +++ b/runtime/lua/vim/lsp/callbacks.lua @@ -33,7 +33,6 @@ M['textDocument/publishDiagnostics'] = function(_, _, result) util.buf_diagnostics_underline(bufnr, result.diagnostics) util.buf_diagnostics_virtual_text(bufnr, result.diagnostics) util.buf_diagnostics_signs(bufnr, result.diagnostics) - -- util.set_loclist(result.diagnostics) vim.api.nvim_command("doautocmd User LspDiagnosticsChanged") end diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 803d9443b7..5f0fe8ceb4 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -797,7 +797,7 @@ function M.locations_to_items(locations) for _, d in ipairs(locations) do local start = d.range.start local fname = assert(vim.uri_to_fname(d.uri)) - table.insert(grouped[fname], {start = start, msg= d.message }) + table.insert(grouped[fname], {start = start}) end @@ -824,7 +824,7 @@ function M.locations_to_items(locations) filename = fname, lnum = row + 1, col = col + 1; - text = temp.msg or line; + text = line; }) end end -- cgit From 1fe01b36de72a937ac7be430077df918a659345d Mon Sep 17 00:00:00 2001 From: Jesse Bakker Date: Fri, 24 Jan 2020 12:31:52 +0100 Subject: Use buffer version instead of changedtick for edits --- runtime/lua/vim/lsp.lua | 2 ++ runtime/lua/vim/lsp/util.lua | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index bc0da25ae5..8190542955 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -613,6 +613,8 @@ do if tbl_isempty(all_buffer_active_clients[bufnr] or {}) then return end + + util.buf_versions[bufnr] = changedtick -- Lazy initialize these because clients may not even need them. local incremental_changes = once(function(client) local size_index = encoding_index[client.offset_encoding] diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index b7c7b7f75d..3dfe4d7d02 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -135,7 +135,7 @@ function M.apply_text_document_edit(text_document_edit) local text_document = text_document_edit.textDocument local bufnr = vim.uri_to_bufnr(text_document.uri) -- TODO(ashkan) check this is correct. - if api.nvim_buf_get_changedtick(bufnr) > text_document.version then + if (M.buf_versions[bufnr] or 0) > text_document.version then print("Buffer ", text_document.uri, " newer than edits.") return end @@ -868,5 +868,7 @@ function M.character_offset(buf, row, col) return str_utfindex(line, col) end +M.buf_versions = {} + return M -- vim:sw=2 ts=2 et -- cgit From f157fdef7eccba6601c6d3b1475847466b60c25b Mon Sep 17 00:00:00 2001 From: Hirokazu Hata Date: Sat, 22 Feb 2020 21:14:10 +0900 Subject: lsp: add bufnr to callback function arguments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DocumentSymbol type doesn't have location field. So when we'll add 'textDocument/documentSymbol’ handler, we can't decide which file have we jump to. --- runtime/lua/vim/lsp.lua | 12 ++++++------ runtime/lua/vim/lsp/buf.lua | 5 +++++ 2 files changed, 11 insertions(+), 6 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 71ec3cb6c4..23eac45046 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -101,7 +101,7 @@ local function for_each_buffer_client(bufnr, callback) for client_id in pairs(client_ids) do local client = active_clients[client_id] if client then - callback(client, client_id) + callback(client, client_id, bufnr) end end end @@ -520,7 +520,7 @@ function lsp.start_client(config) end --- Checks capabilities before rpc.request-ing. - function client.request(method, params, callback) + function client.request(method, params, callback, bufnr) if not callback then callback = resolve_callback(method) or error("not found: request callback for client "..client.name) @@ -536,7 +536,7 @@ function lsp.start_client(config) return end return rpc.request(method, params, function(err, result) - callback(err, method, result, client_id) + callback(err, method, result, client_id, bufnr) end) end @@ -836,8 +836,8 @@ function lsp.buf_request(bufnr, method, params, callback) callback = { callback, 'f', true }; } local client_request_ids = {} - for_each_buffer_client(bufnr, function(client, client_id) - local request_success, request_id = client.request(method, params, callback) + for_each_buffer_client(bufnr, function(client, client_id, resolved_bufnr) + local request_success, request_id = client.request(method, params, callback, resolved_bufnr) -- This could only fail if the client shut down in the time since we looked -- it up and we did the request, which should be rare. @@ -874,7 +874,7 @@ end function lsp.buf_request_sync(bufnr, method, params, timeout_ms) local request_results = {} local result_count = 0 - local function _callback(err, _method, result, client_id) + local function _callback(err, _method, result, client_id, bufnr) request_results[client_id] = { error = err, result = result } result_count = result_count + 1 end diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 82aeccd4db..fc9e10cb73 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -138,6 +138,11 @@ function M.references(context) request('textDocument/references', params) end +function M.document_symbol() + local params = { textDocument = util.make_text_document_params() } + request('textDocument/documentSymbol', params) +end + --- Send request to server to resolve document highlights for the --- current text document position. This request can be associated --- to key mapping or to events such as `CursorHold`, eg: -- cgit From 16262472cda88d0a4dc5c6729cfef36264569324 Mon Sep 17 00:00:00 2001 From: Hirokazu Hata Date: Sat, 22 Feb 2020 21:20:38 +0900 Subject: lsp: add 'textDocument/documentSymbol’ callback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spec: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentSymbol --- runtime/lua/vim/lsp.lua | 9 ++++--- runtime/lua/vim/lsp/callbacks.lua | 10 +++++-- runtime/lua/vim/lsp/protocol.lua | 26 +++++++++--------- runtime/lua/vim/lsp/util.lua | 57 +++++++++++++++++++++++++++++++++------ 4 files changed, 75 insertions(+), 27 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 23eac45046..8af20ea1f9 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -525,14 +525,15 @@ function lsp.start_client(config) callback = resolve_callback(method) or error("not found: request callback for client "..client.name) end - local _ = log.debug() and log.debug(log_prefix, "client.request", client_id, method, params, callback) + local _ = log.debug() and log.debug(log_prefix, "client.request", client_id, method, params, callback, bufnr) -- TODO keep these checks or just let it go anyway? if (not client.resolved_capabilities.hover and method == 'textDocument/hover') or (not client.resolved_capabilities.signature_help and method == 'textDocument/signatureHelp') or (not client.resolved_capabilities.goto_definition and method == 'textDocument/definition') or (not client.resolved_capabilities.implementation and method == 'textDocument/implementation') + or (not client.resolved_capabilities.document_symbol and method == 'textDocument/documentSymbol') then - callback(unsupported_method(method), method, nil, client_id) + callback(unsupported_method(method), method, nil, client_id, bufnr) return end return rpc.request(method, params, function(err, result) @@ -874,7 +875,7 @@ end function lsp.buf_request_sync(bufnr, method, params, timeout_ms) local request_results = {} local result_count = 0 - local function _callback(err, _method, result, client_id, bufnr) + local function _callback(err, _method, result, client_id) request_results[client_id] = { error = err, result = result } result_count = result_count + 1 end @@ -905,7 +906,7 @@ function lsp.buf_notify(bufnr, method, params) method = { method, 's' }; } local resp = false - for_each_buffer_client(bufnr, function(client, _client_id) + for_each_buffer_client(bufnr, function(client, _client_id, _resolved_bufnr) if client.rpc.notify(method, params) then resp = true end end) return resp diff --git a/runtime/lua/vim/lsp/callbacks.lua b/runtime/lua/vim/lsp/callbacks.lua index 99093216d9..63b5c4d493 100644 --- a/runtime/lua/vim/lsp/callbacks.lua +++ b/runtime/lua/vim/lsp/callbacks.lua @@ -38,7 +38,13 @@ end M['textDocument/references'] = function(_, _, result) if not result then return end - util.set_qflist(result) + util.set_qflist(util.locations_to_items(result)) +end + +M['textDocument/documentSymbol'] = function(_, _, result, _, bufnr) + if not result or vim.tbl_isempty(result) then return end + + util.set_qflist(util.symbols_to_items(result, bufnr)) api.nvim_command("copen") api.nvim_command("wincmd p") end @@ -97,7 +103,7 @@ local function location_callback(_, method, result) end util.jump_to_location(result[1]) if #result > 1 then - util.set_qflist(result) + util.set_qflist(util.locations_to_items(result)) api.nvim_command("copen") api.nvim_command("wincmd p") end diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index f64b0b50e7..41e8119c8c 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -663,19 +663,19 @@ function protocol.make_client_capabilities() documentHighlight = { dynamicRegistration = false }; - -- documentSymbol = { - -- dynamicRegistration = false; - -- symbolKind = { - -- valueSet = (function() - -- local res = {} - -- for k in pairs(protocol.SymbolKind) do - -- if type(k) == 'string' then table.insert(res, k) end - -- end - -- return res - -- end)(); - -- }; - -- hierarchicalDocumentSymbolSupport = false; - -- }; + documentSymbol = { + dynamicRegistration = false; + symbolKind = { + valueSet = (function() + local res = {} + for k in pairs(protocol.SymbolKind) do + if type(k) == 'number' then table.insert(res, k) end + end + return res + end)(); + }; + hierarchicalDocumentSymbolSupport = true; + }; }; workspace = nil; experimental = nil; diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 5f0fe8ceb4..72c84b8471 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -834,23 +834,60 @@ function M.locations_to_items(locations) return items end --- locations is Location[] --- Only sets for the current window. -function M.set_loclist(locations) +function M.set_loclist(items) vim.fn.setloclist(0, {}, ' ', { title = 'Language Server'; - items = M.locations_to_items(locations); + items = items; }) end --- locations is Location[] -function M.set_qflist(locations) +function M.set_qflist(items) vim.fn.setqflist({}, ' ', { title = 'Language Server'; - items = M.locations_to_items(locations); + items = items; }) end +--- Convert symbols to quickfix list items +--- +--@symbols DocumentSymbol[] or SymbolInformation[] +function M.symbols_to_items(symbols, bufnr) + local function _symbols_to_items(_symbols, _items, _bufnr) + for _, symbol in ipairs(_symbols) do + if symbol.location then -- SymbolInformation type + local range = symbol.location.range + local kind = protocol.SymbolKind[symbol.kind] + table.insert(_items, { + filename = vim.uri_to_fname(symbol.location.uri), + lnum = range.start.line + 1, + col = range.start.character + 1, + kind = kind, + text = '['..kind..'] '..symbol.name, + }) + elseif symbol.range then -- DocumentSymbole type + local kind = protocol.SymbolKind[symbol.kind] + table.insert(_items, { + -- bufnr = _bufnr, + filename = vim.api.nvim_buf_get_name(_bufnr), + lnum = symbol.range.start.line + 1, + col = symbol.range.start.character + 1, + kind = kind, + text = '['..kind..'] '..symbol.name + }) + if symbol.children then + for _, child in ipairs(symbol) do + for _, v in ipairs(_symbols_to_items(child, _items, _bufnr)) do + vim.list_extend(_items, v) + end + end + end + end + end + return _items + end + return _symbols_to_items(symbols, {}, bufnr) +end + -- Remove empty lines from the beginning and end. function M.trim_empty_lines(lines) local start = 1 @@ -903,11 +940,15 @@ function M.make_position_params() local line = api.nvim_buf_get_lines(0, row, row+1, true)[1] col = str_utfindex(line, col) return { - textDocument = { uri = vim.uri_from_bufnr(0) }; + textDocument = M.make_text_document_params(); position = { line = row; character = col; } } end +function M.make_text_document_params() + return { uri = vim.uri_from_bufnr(0) } +end + -- @param buf buffer handle or 0 for current. -- @param row 0-indexed line -- @param col 0-indexed byte offset in line -- cgit From e35ff7371f4a61621587744a7620200380abbbe9 Mon Sep 17 00:00:00 2001 From: Hirokazu Hata Date: Mon, 2 Mar 2020 16:38:43 +0900 Subject: lua: add vim.tbl_len() #11889 --- runtime/lua/vim/shared.lua | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 498992aa2e..1bf1c63fd7 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -356,6 +356,24 @@ function vim.tbl_islist(t) end end +--- Counts the number of non-nil values in table `t`. +--- +---
+--- vim.tbl_count({ a=1, b=2 }) => 2
+--- vim.tbl_count({ 1, 2 }) => 2
+--- 
+--- +--@see https://github.com/Tieske/Penlight/blob/master/lua/pl/tablex.lua +--@param Table +--@returns Number that is the number of the value in table +function vim.tbl_count(t) + vim.validate{t={t,'t'}} + + local count = 0 + for _ in pairs(t) do count = count + 1 end + return count +end + --- Trim whitespace (Lua pattern "%s") from both sides of a string. --- --@see https://www.lua.org/pil/20.2.html -- cgit From fbc4c4fd36bef6f10f75af675985167d5fab4699 Mon Sep 17 00:00:00 2001 From: Hirokazu Hata Date: Mon, 2 Mar 2020 22:27:30 +0900 Subject: lsp: make showMessage and logMessage callbacks different (#11942) According to the LSP specification, showMessage is what is displayed and logMessage is what is stored. Since these are different things, I devide the callback into those that match. --- runtime/lua/vim/lsp/callbacks.lua | 25 +++++++++++++++++++++---- runtime/lua/vim/lsp/log.lua | 1 - 2 files changed, 21 insertions(+), 5 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/callbacks.lua b/runtime/lua/vim/lsp/callbacks.lua index 63b5c4d493..644c12f98c 100644 --- a/runtime/lua/vim/lsp/callbacks.lua +++ b/runtime/lua/vim/lsp/callbacks.lua @@ -209,7 +209,27 @@ M['textDocument/documentHighlight'] = function(_, _, result, _) util.buf_highlight_references(bufnr, result) end -local function log_message(_, _, result, client_id) +M['window/logMessage'] = function(_, _, result, client_id) + local message_type = result.type + local message = result.message + local client = vim.lsp.get_client_by_id(client_id) + local client_name = client and client.name or string.format("id=%d", client_id) + if not client then + err_message("LSP[", client_name, "] client has shut down after sending the message") + end + if message_type == protocol.MessageType.Error then + log.error(message) + elseif message_type == protocol.MessageType.Warning then + log.warn(message) + elseif message_type == protocol.MessageType.Info then + log.info(message) + else + log.debug(message) + end + return result +end + +M['window/showMessage'] = function(_, _, result, client_id) local message_type = result.type local message = result.message local client = vim.lsp.get_client_by_id(client_id) @@ -226,9 +246,6 @@ local function log_message(_, _, result, client_id) return result end -M['window/showMessage'] = log_message -M['window/logMessage'] = log_message - -- Add boilerplate error validation and logging for all of these. for k, fn in pairs(M) do M[k] = function(err, method, params, client_id) diff --git a/runtime/lua/vim/lsp/log.lua b/runtime/lua/vim/lsp/log.lua index 974eaae38c..c0db5e5485 100644 --- a/runtime/lua/vim/lsp/log.lua +++ b/runtime/lua/vim/lsp/log.lua @@ -13,7 +13,6 @@ log.levels = { INFO = 2; WARN = 3; ERROR = 4; - -- FATAL = 4; } -- Default log level is warn. -- cgit From ccb038dc6aaf72e052ef8afb810f84a331d4ccec Mon Sep 17 00:00:00 2001 From: Andrey Avramenko Date: Fri, 6 Mar 2020 19:40:46 +0200 Subject: LSP/completion: add textEdit support According to lsp specification, value of insertText should be ignored if textEdit is provided. --- runtime/lua/vim/lsp/util.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 72c84b8471..533bcf2779 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -161,7 +161,7 @@ end -- So we exclude completion candidates whose prefix does not match. local function remove_unmatch_completion_items(items, prefix) return vim.tbl_filter(function(item) - local word = item.insertText or item.label + local word = (item.textEdit and item.textEdit.newText) or item.insertText or item.label return vim.startswith(word, prefix) end, items) end @@ -193,7 +193,7 @@ function M.text_document_completion_list_to_complete_items(result, prefix) end end - local word = completion_item.insertText or completion_item.label + local word = (completion_item.textEdit and completion_item.textEdit.newText) or completion_item.insertText or completion_item.label table.insert(matches, { word = word, abbr = completion_item.label, -- cgit From 4139678f97ee556ab142031a1ed5c7580278b64f Mon Sep 17 00:00:00 2001 From: Hirokazu Hata Date: Mon, 30 Mar 2020 21:30:24 +0900 Subject: vim.uri: fix uri_to_fname (#12059) fix: #12056 If the colon of the drive letter of windows is URI encoded, it doesn't match the expected pattern, so decode it first. --- runtime/lua/vim/uri.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/uri.lua b/runtime/lua/vim/uri.lua index 1065f84f4c..d91fb7ffd3 100644 --- a/runtime/lua/vim/uri.lua +++ b/runtime/lua/vim/uri.lua @@ -70,6 +70,7 @@ local function uri_from_bufnr(bufnr) end local function uri_to_fname(uri) + uri = uri_decode(uri) -- TODO improve this. if is_windows_file_uri(uri) then uri = uri:gsub('^file:///', '') @@ -77,7 +78,7 @@ local function uri_to_fname(uri) else uri = uri:gsub('^file://', '') end - return uri_decode(uri) + return uri end -- Return or create a buffer for a uri. -- cgit From 51b4fc4778896775da5854fe6a04d00445b72919 Mon Sep 17 00:00:00 2001 From: George Zhao Date: Fri, 17 Apr 2020 00:30:03 +0800 Subject: lsp: provide a default for missing reference kind (#12127) Fix #12122 >Error executing vim.schedule lua callback: /usr/local/share/nvim/runtime/lua/vim/lsp/util.lua:560: Expected lua string Some lsp server(e.g. https://github.com/bash-lsp/bash-language-server) not have kind in reference, reference["kind"] is nil --- runtime/lua/vim/lsp/util.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 5dd010f2a4..1a8d7dc084 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -725,7 +725,8 @@ do [protocol.DocumentHighlightKind.Read] = "LspReferenceRead"; [protocol.DocumentHighlightKind.Write] = "LspReferenceWrite"; } - highlight_range(bufnr, reference_ns, document_highlight_kind[reference["kind"]], start_pos, end_pos) + local kind = reference["kind"] or protocol.DocumentHighlightKind.Text + highlight_range(bufnr, reference_ns, document_highlight_kind[kind], start_pos, end_pos) end end -- cgit From 1fb44ba8356e06b9d90a43599c4526ad4d625618 Mon Sep 17 00:00:00 2001 From: Thomas Vigouroux Date: Fri, 17 Apr 2020 17:24:04 +0200 Subject: treesitter: escape backslashes in queries Treesitter changed their decoders and apparently thus causing this change. This decoder change happened on ee9a3c0ebb218990cf391ed987be7f2448c54a73. --- runtime/lua/vim/treesitter.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua index 8dacfa11cf..1836227540 100644 --- a/runtime/lua/vim/treesitter.lua +++ b/runtime/lua/vim/treesitter.lua @@ -124,7 +124,7 @@ end function M.parse_query(lang, query) M.require_language(lang) local self = setmetatable({}, Query) - self.query = vim._ts_parse_query(lang, query) + self.query = vim._ts_parse_query(lang, vim.fn.escape(query,'\\')) self.info = self.query:inspect() self.captures = self.info.captures self.regexes = {} -- cgit From 7d4451c65785ef7e781bf5029da059fb77b728b7 Mon Sep 17 00:00:00 2001 From: Ghjuvan Lacambre Date: Sat, 18 Apr 2020 18:21:08 +0200 Subject: LSP: fix breakage when severity isn't specified (#12027) Before this commit, the LSP client would throw errors when messages without severity would be sent by the server. We make severity default to `Error` as a kludge before proper heuristics to discover the severity of a message are found. --- runtime/lua/vim/lsp/callbacks.lua | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/callbacks.lua b/runtime/lua/vim/lsp/callbacks.lua index 644c12f98c..c403a3fd51 100644 --- a/runtime/lua/vim/lsp/callbacks.lua +++ b/runtime/lua/vim/lsp/callbacks.lua @@ -29,6 +29,17 @@ M['textDocument/publishDiagnostics'] = function(_, _, result) return end util.buf_clear_diagnostics(bufnr) + + -- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#diagnostic + -- The diagnostic's severity. Can be omitted. If omitted it is up to the + -- client to interpret diagnostics as error, warning, info or hint. + -- TODO: Replace this with server-specific heuristics to infer severity. + for _, diagnostic in ipairs(result.diagnostics) do + if diagnostic.severity == nil then + diagnostic.severity = protocol.DiagnosticSeverity.Error + end + end + util.buf_diagnostics_save_positions(bufnr, result.diagnostics) util.buf_diagnostics_underline(bufnr, result.diagnostics) util.buf_diagnostics_virtual_text(bufnr, result.diagnostics) -- cgit From c5466ba6ef8333183e1c43c7e762e44539fb2358 Mon Sep 17 00:00:00 2001 From: Hirokazu Hata Date: Sun, 19 Apr 2020 02:04:47 +0900 Subject: lsp: replace the event that closes the signature help preview window from InsertCharPre to CursolMovedI (#11954) In the case of InsertCharPre, it is inconvenient because the signature help is displayed when backspaced in insert mode, so change it to CursolMovedI. --- runtime/lua/vim/lsp/util.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 1a8d7dc084..763e9719a7 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -522,8 +522,7 @@ function M.open_floating_preview(contents, filetype, opts) end api.nvim_buf_set_lines(floating_bufnr, 0, -1, true, contents) api.nvim_buf_set_option(floating_bufnr, 'modifiable', false) - -- TODO make InsertCharPre disappearing optional? - api.nvim_command("autocmd CursorMoved,BufHidden,InsertCharPre ++once lua pcall(vim.api.nvim_win_close, "..floating_winnr..", true)") + M.close_preview_autocmd({"CursorMoved", "CursorMovedI", "BufHidden"}, floating_winnr) return floating_bufnr, floating_winnr end -- cgit From bf0f74586153dfa8d550e1cfefd83ca9e0354171 Mon Sep 17 00:00:00 2001 From: Tristan Konolige Date: Sat, 18 Apr 2020 17:04:37 -0600 Subject: lua: allow deepcopy of functions (#12136) --- runtime/lua/vim/shared.lua | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 1bf1c63fd7..d18fcfaf95 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -8,6 +8,9 @@ local vim = vim or {} --- Returns a deep copy of the given object. Non-table objects are copied as --- in a typical Lua assignment, whereas table objects are copied recursively. +--- Functions are naively copied, so functions in the copied table point to the +--- same functions as those in the input table. Userdata and threads are not +--- copied and will throw an error. --- --@param orig Table to copy --@returns New table of copied keys and (nested) values. @@ -34,10 +37,16 @@ vim.deepcopy = (function() string = _id, ['nil'] = _id, boolean = _id, + ['function'] = _id, } return function(orig) - return deepcopy_funcs[type(orig)](orig) + local f = deepcopy_funcs[type(orig)] + if f then + return f(orig) + else + error("Cannot deepcopy object of type "..type(orig)) + end end end)() -- cgit From adec9fb444d4a50637f097eb2fef1abbced3f385 Mon Sep 17 00:00:00 2001 From: Hirokazu Hata Date: Mon, 20 Apr 2020 06:40:54 +0900 Subject: lsp: export convert_signature_help_to_markdown_lines (#11950) This function is also useful for users to create their own `textDocument/signatureHelp` callback function. --- runtime/lua/vim/lsp/callbacks.lua | 61 +-------------------------------------- runtime/lua/vim/lsp/util.lua | 59 +++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 60 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/callbacks.lua b/runtime/lua/vim/lsp/callbacks.lua index c403a3fd51..b6396f5798 100644 --- a/runtime/lua/vim/lsp/callbacks.lua +++ b/runtime/lua/vim/lsp/callbacks.lua @@ -125,72 +125,13 @@ M['textDocument/definition'] = location_callback M['textDocument/typeDefinition'] = location_callback M['textDocument/implementation'] = location_callback ---- Convert SignatureHelp response to preview contents. --- https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#textDocument_signatureHelp -local function signature_help_to_preview_contents(input) - if not input.signatures then - return - end - --The active signature. If omitted or the value lies outside the range of - --`signatures` the value defaults to zero or is ignored if `signatures.length - --=== 0`. Whenever possible implementors should make an active decision about - --the active signature and shouldn't rely on a default value. - local contents = {} - local active_signature = input.activeSignature or 0 - -- If the activeSignature is not inside the valid range, then clip it. - if active_signature >= #input.signatures then - active_signature = 0 - end - local signature = input.signatures[active_signature + 1] - if not signature then - return - end - vim.list_extend(contents, vim.split(signature.label, '\n', true)) - if signature.documentation then - util.convert_input_to_markdown_lines(signature.documentation, contents) - end - if input.parameters then - local active_parameter = input.activeParameter or 0 - -- If the activeParameter is not inside the valid range, then clip it. - if active_parameter >= #input.parameters then - active_parameter = 0 - end - local parameter = signature.parameters and signature.parameters[active_parameter] - if parameter then - --[=[ - --Represents a parameter of a callable-signature. A parameter can - --have a label and a doc-comment. - interface ParameterInformation { - --The label of this parameter information. - -- - --Either a string or an inclusive start and exclusive end offsets within its containing - --signature label. (see SignatureInformation.label). The offsets are based on a UTF-16 - --string representation as `Position` and `Range` does. - -- - --*Note*: a label of type string should be a substring of its containing signature label. - --Its intended use case is to highlight the parameter label part in the `SignatureInformation.label`. - label: string | [number, number]; - --The human-readable doc-comment of this parameter. Will be shown - --in the UI but can be omitted. - documentation?: string | MarkupContent; - } - --]=] - -- TODO highlight parameter - if parameter.documentation then - util.convert_input_to_markdown_lines(parameter.documentation, contents) - end - end - end - return contents -end - M['textDocument/signatureHelp'] = function(_, method, result) util.focusable_preview(method, function() if not (result and result.signatures and result.signatures[1]) then return { 'No signature available' } end -- TODO show popup when signatures is empty? - local lines = signature_help_to_preview_contents(result) + local lines = util.convert_signature_help_to_markdown_lines(result) lines = util.trim_empty_lines(lines) if vim.tbl_isempty(lines) then return { 'No signature available' } diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 763e9719a7..82a4223102 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -275,6 +275,65 @@ function M.convert_input_to_markdown_lines(input, contents) return contents end +--- Convert SignatureHelp response to markdown lines. +-- https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#textDocument_signatureHelp +function M.convert_signature_help_to_markdown_lines(signature_help) + if not signature_help.signatures then + return + end + --The active signature. If omitted or the value lies outside the range of + --`signatures` the value defaults to zero or is ignored if `signatures.length + --=== 0`. Whenever possible implementors should make an active decision about + --the active signature and shouldn't rely on a default value. + local contents = {} + local active_signature = signature_help.activeSignature or 0 + -- If the activeSignature is not inside the valid range, then clip it. + if active_signature >= #signature_help.signatures then + active_signature = 0 + end + local signature = signature_help.signatures[active_signature + 1] + if not signature then + return + end + vim.list_extend(contents, vim.split(signature.label, '\n', true)) + if signature.documentation then + M.convert_input_to_markdown_lines(signature.documentation, contents) + end + if signature_help.parameters then + local active_parameter = signature_help.activeParameter or 0 + -- If the activeParameter is not inside the valid range, then clip it. + if active_parameter >= #signature_help.parameters then + active_parameter = 0 + end + local parameter = signature.parameters and signature.parameters[active_parameter] + if parameter then + --[=[ + --Represents a parameter of a callable-signature. A parameter can + --have a label and a doc-comment. + interface ParameterInformation { + --The label of this parameter information. + -- + --Either a string or an inclusive start and exclusive end offsets within its containing + --signature label. (see SignatureInformation.label). The offsets are based on a UTF-16 + --string representation as `Position` and `Range` does. + -- + --*Note*: a label of type string should be a substring of its containing signature label. + --Its intended use case is to highlight the parameter label part in the `SignatureInformation.label`. + label: string | [number, number]; + --The human-readable doc-comment of this parameter. Will be shown + --in the UI but can be omitted. + documentation?: string | MarkupContent; + } + --]=] + -- TODO highlight parameter + if parameter.documentation then + M.convert_input_help_to_markdown_lines(parameter.documentation, contents) + end + end + end + return contents +end + function M.make_floating_popup_options(width, height, opts) validate { opts = { opts, 't', true }; -- cgit From e6cfc1b158506f8d2a4b598d07051b7f3de5f964 Mon Sep 17 00:00:00 2001 From: Andrey Avramenko Date: Mon, 20 Apr 2020 01:59:09 +0300 Subject: LSP/completion: Add completion text helper function --- runtime/lua/vim/lsp/util.lua | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 533bcf2779..5217464c22 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -157,11 +157,23 @@ local function sort_completion_items(items) end end +-- Returns text that should be inserted when selecting completion item. The precedence is as follows: +-- textEdit.newText > insertText > label +-- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion +local function get_completion_word(item) + if item.textEdit ~= nil and item.textEdit.newText ~= nil then + return item.textEdit.newText + elseif item.insertText ~= nil then + return item.insertText + end + return item.label +end + -- Some lanuguage servers return complementary candidates whose prefixes do not match are also returned. -- So we exclude completion candidates whose prefix does not match. local function remove_unmatch_completion_items(items, prefix) return vim.tbl_filter(function(item) - local word = (item.textEdit and item.textEdit.newText) or item.insertText or item.label + local word = get_completion_word(item) return vim.startswith(word, prefix) end, items) end @@ -193,7 +205,7 @@ function M.text_document_completion_list_to_complete_items(result, prefix) end end - local word = (completion_item.textEdit and completion_item.textEdit.newText) or completion_item.insertText or completion_item.label + local word = get_completion_word(completion_item) table.insert(matches, { word = word, abbr = completion_item.label, -- cgit From 0c637898f991ff9b90e695065a0136dd9c37106a Mon Sep 17 00:00:00 2001 From: Khangal Date: Tue, 21 Apr 2020 21:16:58 +0800 Subject: lsp: textDocument/definition can return Location or Location[] (#12014) * https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition Co-authored-by: Khangal Jargalsaikhan --- runtime/lua/vim/lsp/callbacks.lua | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/callbacks.lua b/runtime/lua/vim/lsp/callbacks.lua index b6396f5798..bcadd0e2c3 100644 --- a/runtime/lua/vim/lsp/callbacks.lua +++ b/runtime/lua/vim/lsp/callbacks.lua @@ -112,11 +112,20 @@ local function location_callback(_, method, result) local _ = log.info() and log.info(method, 'No location found') return nil end - util.jump_to_location(result[1]) - if #result > 1 then - util.set_qflist(util.locations_to_items(result)) - api.nvim_command("copen") - api.nvim_command("wincmd p") + + -- textDocument/definition can return Location or Location[] + -- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition + + if vim.tbl_islist(result) then + util.jump_to_location(result[1]) + + if #result > 1 then + util.set_qflist(util.locations_to_items(result)) + api.nvim_command("copen") + api.nvim_command("wincmd p") + end + else + util.jump_to_location(result) end end -- cgit From 633322a020b0253aeb8c6eaeeed17d8a8fb53229 Mon Sep 17 00:00:00 2001 From: Hirokazu Hata Date: Tue, 21 Apr 2020 22:42:48 +0900 Subject: lsp: do not assert even if the code does not exist in ErrorCodes (#11981) There is ErrorCodes in the LSP specification, but in ResponseError.code it is not used and the actual type is number. Some language servers response original error cods and this is valid spec. So we shouldn't assert even if the code does not exist in ErrorCodes. ref: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#responseMessage --- runtime/lua/vim/lsp/rpc.lua | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua index 74d73da31f..dad1dc11f1 100644 --- a/runtime/lua/vim/lsp/rpc.lua +++ b/runtime/lua/vim/lsp/rpc.lua @@ -140,14 +140,23 @@ local function format_rpc_error(err) validate { err = { err, 't' }; } - local code_name = assert(protocol.ErrorCodes[err.code], "err.code is invalid") - local message_parts = {"RPC", code_name} + + -- There is ErrorCodes in the LSP specification, + -- but in ResponseError.code it is not used and the actual type is number. + local code + if protocol.ErrorCodes[err.code] then + code = string.format("code_name = %s,", protocol.ErrorCodes[err.code]) + else + code = string.format("code_name = unknown, code = %s,", err.code) + end + + local message_parts = {"RPC[Error]", code} if err.message then - table.insert(message_parts, "message = ") + table.insert(message_parts, "message =") table.insert(message_parts, string.format("%q", err.message)) end if err.data then - table.insert(message_parts, "data = ") + table.insert(message_parts, "data =") table.insert(message_parts, vim.inspect(err.data)) end return table.concat(message_parts, ' ') -- cgit From deb4566cab91a487f2b0b0b5b4b427138f377afe Mon Sep 17 00:00:00 2001 From: Thore Weilbier Date: Thu, 23 Apr 2020 13:36:19 +0200 Subject: lsp: callback for references now opens qf (#12171) In contrast to other callbacks for LSP requests like `textDocument/documentSymbols`, does the one for references not open the quickfix window after the quickfix list was filled. This left the user in a situation he don't know what or if something had happen. Related to: neovim/neovim#12170 --- runtime/lua/vim/lsp/callbacks.lua | 2 ++ 1 file changed, 2 insertions(+) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/callbacks.lua b/runtime/lua/vim/lsp/callbacks.lua index bcadd0e2c3..9c30085f37 100644 --- a/runtime/lua/vim/lsp/callbacks.lua +++ b/runtime/lua/vim/lsp/callbacks.lua @@ -50,6 +50,8 @@ end M['textDocument/references'] = function(_, _, result) if not result then return end util.set_qflist(util.locations_to_items(result)) + api.nvim_command("copen") + api.nvim_command("wincmd p") end M['textDocument/documentSymbol'] = function(_, _, result, _, bufnr) -- cgit From 78d58eaf61b361ed9b2849ecc1afc99897c01058 Mon Sep 17 00:00:00 2001 From: Hirokazu Hata Date: Sat, 25 Apr 2020 21:58:35 +0900 Subject: lsp: remove buffer version on buffer_detach (#12029) When we save the buffer, the buffer is detached and attached again. So the client also needs to remove the buffer version once. --- runtime/lua/vim/lsp.lua | 2 ++ runtime/lua/vim/lsp/util.lua | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index afff4d9900..17135e078c 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -198,6 +198,7 @@ local function text_document_did_open_handler(bufnr, client) } } client.notify('textDocument/didOpen', params) + util.buf_versions[bufnr] = params.textDocument.version end --- LSP client object. @@ -722,6 +723,7 @@ function lsp.buf_attach_client(bufnr, client_id) client.notify('textDocument/didClose', params) end end) + util.buf_versions[bufnr] = nil all_buffer_active_clients[bufnr] = nil end; -- TODO if we know all of the potential clients ahead of time, then we diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index ce5baf5b4b..4c73bf8c5f 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -134,8 +134,7 @@ end function M.apply_text_document_edit(text_document_edit) local text_document = text_document_edit.textDocument local bufnr = vim.uri_to_bufnr(text_document.uri) - -- TODO(ashkan) check this is correct. - if (M.buf_versions[bufnr] or 0) > text_document.version then + if M.buf_versions[bufnr] > text_document.version then print("Buffer ", text_document.uri, " newer than edits.") return end -- cgit From ef0398fe88e6cc74f33fb20519997774168d7832 Mon Sep 17 00:00:00 2001 From: Mathias Fußenegger Date: Sat, 25 Apr 2020 15:46:58 +0200 Subject: LSP: Expose diagnostics grouped by bufnr (#11932) Expose `vim.lsp.buf.diagnostics_by_buf` This makes it easier to customize the diagnostics behavior. For example to defer the update they can override the `textDocument/publishDiagnostics` callback to only call `buf_diagnostics_save_positions` and then defer the other actions to a autocmd event. --- runtime/lua/vim/lsp/util.lua | 86 +++++++++++++++++++++++++++----------------- 1 file changed, 54 insertions(+), 32 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 4c73bf8c5f..80e4399fe1 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -6,6 +6,31 @@ local list_extend = vim.list_extend local M = {} +--- Diagnostics received from the server via `textDocument/publishDiagnostics` +-- by buffer. +-- +-- {: {diagnostics}} +-- +-- This contains only entries for active buffers. Entries for detached buffers +-- are discarded. +-- +-- If you override the `textDocument/publishDiagnostic` callback, +-- this will be empty unless you call `buf_diagnostics_save_positions`. +-- +-- +-- Diagnostic is: +-- +-- { +-- range: Range +-- message: string +-- severity?: DiagnosticSeverity +-- code?: number | string +-- source?: string +-- tags?: DiagnosticTag[] +-- relatedInformation?: DiagnosticRelatedInformation[] +-- } +M.diagnostics_by_buf = {} + local split = vim.split local function split_lines(value) return split(value, '\n', true) @@ -635,8 +660,6 @@ local function highlight_range(bufnr, ns, hiname, start, finish) end do - local all_buffer_diagnostics = {} - local diagnostic_ns = api.nvim_create_namespace("vim_lsp_diagnostics") local reference_ns = api.nvim_create_namespace("vim_lsp_references") local sign_ns = 'vim_lsp_signs' @@ -692,13 +715,12 @@ do -- if #marks == 0 then -- return -- end - -- local buffer_diagnostics = all_buffer_diagnostics[bufnr] local lines = {"Diagnostics:"} local highlights = {{0, "Bold"}} - local buffer_diagnostics = all_buffer_diagnostics[bufnr] + local buffer_diagnostics = M.diagnostics_by_buf[bufnr] if not buffer_diagnostics then return end - local line_diagnostics = buffer_diagnostics[line] + local line_diagnostics = M.diagnostics_group_by_line(buffer_diagnostics[line]) if not line_diagnostics then return end for i, diagnostic in ipairs(line_diagnostics) do @@ -726,6 +748,8 @@ do return popup_bufnr, winnr end + --- Saves the diagnostics (Diagnostic[]) into diagnostics_by_buf + -- function M.buf_diagnostics_save_positions(bufnr, diagnostics) validate { bufnr = {bufnr, 'n', true}; @@ -734,28 +758,15 @@ do if not diagnostics then return end bufnr = bufnr == 0 and api.nvim_get_current_buf() or bufnr - if not all_buffer_diagnostics[bufnr] then + if not M.diagnostics_by_buf[bufnr] then -- Clean up our data when the buffer unloads. api.nvim_buf_attach(bufnr, false, { on_detach = function(b) - all_buffer_diagnostics[b] = nil + M.diagnostics_by_buf[b] = nil end }) end - all_buffer_diagnostics[bufnr] = {} - local buffer_diagnostics = all_buffer_diagnostics[bufnr] - - for _, diagnostic in ipairs(diagnostics) do - local start = diagnostic.range.start - -- local mark_id = api.nvim_buf_set_extmark(bufnr, diagnostic_ns, 0, start.line, 0, {}) - -- buffer_diagnostics[mark_id] = diagnostic - local line_diagnostics = buffer_diagnostics[start.line] - if not line_diagnostics then - line_diagnostics = {} - buffer_diagnostics[start.line] = line_diagnostics - end - table.insert(line_diagnostics, diagnostic) - end + M.diagnostics_by_buf[bufnr] = diagnostics end function M.buf_diagnostics_underline(bufnr, diagnostics) @@ -799,15 +810,26 @@ do end end - function M.buf_diagnostics_virtual_text(bufnr, diagnostics) - local buffer_line_diagnostics = all_buffer_diagnostics[bufnr] - if not buffer_line_diagnostics then - M.buf_diagnostics_save_positions(bufnr, diagnostics) + function M.diagnostics_group_by_line(diagnostics) + if not diagnostics then return end + local diagnostics_by_line = {} + for _, diagnostic in ipairs(diagnostics) do + local start = diagnostic.range.start + local line_diagnostics = diagnostics_by_line[start.line] + if not line_diagnostics then + line_diagnostics = {} + diagnostics_by_line[start.line] = line_diagnostics + end + table.insert(line_diagnostics, diagnostic) end - buffer_line_diagnostics = all_buffer_diagnostics[bufnr] - if not buffer_line_diagnostics then + return diagnostics_by_line + end + + function M.buf_diagnostics_virtual_text(bufnr, diagnostics) + if not diagnostics then return end + local buffer_line_diagnostics = M.diagnostics_group_by_line(diagnostics) for line, line_diags in pairs(buffer_line_diagnostics) do local virt_texts = {} for i = 1, #line_diags - 1 do @@ -821,12 +843,12 @@ do end function M.buf_diagnostics_count(kind) local bufnr = vim.api.nvim_get_current_buf() - local buffer_line_diagnostics = all_buffer_diagnostics[bufnr] - if not buffer_line_diagnostics then return end + local diagnostics = M.diagnostics_by_buf[bufnr] + if not diagnostics then return end local count = 0 - for _, line_diags in pairs(buffer_line_diagnostics) do - for _, diag in ipairs(line_diags) do - if protocol.DiagnosticSeverity[kind] == diag.severity then count = count + 1 end + for _, diagnostic in pairs(diagnostics) do + if protocol.DiagnosticSeverity[kind] == diagnostic.severity then + count = count + 1 end end return count -- cgit From 68511924d02af03812ae4b56aea6d94009f8096e Mon Sep 17 00:00:00 2001 From: Thore Weilbier Date: Sun, 26 Apr 2020 06:47:48 +0200 Subject: LSP: remove obsolete "peek definition" code #12178 The method with the name 'textDocument/peekDefinition' is not part of the official language server protocol specification. Therefore no language server can/will support this. Thereby all related code and documentation as been removed. --- runtime/lua/vim/lsp/buf.lua | 6 ------ runtime/lua/vim/lsp/callbacks.lua | 15 --------------- runtime/lua/vim/lsp/util.lua | 25 ------------------------- 3 files changed, 46 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index fc9e10cb73..587d1f52e9 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -32,12 +32,6 @@ function M.hover() request('textDocument/hover', params) end -function M.peek_definition() - local params = util.make_position_params() - request('textDocument/peekDefinition', params) -end - - function M.declaration() local params = util.make_position_params() request('textDocument/declaration', params) diff --git a/runtime/lua/vim/lsp/callbacks.lua b/runtime/lua/vim/lsp/callbacks.lua index 9c30085f37..bd2cbf1ea7 100644 --- a/runtime/lua/vim/lsp/callbacks.lua +++ b/runtime/lua/vim/lsp/callbacks.lua @@ -151,21 +151,6 @@ M['textDocument/signatureHelp'] = function(_, method, result) end) end -M['textDocument/peekDefinition'] = function(_, _, result, _) - if not (result and result[1]) then return end - local loc = result[1] - local bufnr = vim.uri_to_bufnr(loc.uri) or error("not found: "..tostring(loc.uri)) - local start = loc.range.start - local finish = loc.range["end"] - util.open_floating_peek_preview(bufnr, start, finish, { offset_x = 1 }) - local headbuf = util.open_floating_preview({"Peek:"}, nil, { - offset_y = -(finish.line - start.line); - width = finish.character - start.character + 2; - }) - -- TODO(ashkan) change highlight group? - api.nvim_buf_add_highlight(headbuf, -1, 'Keyword', 0, -1) -end - M['textDocument/documentHighlight'] = function(_, _, result, _) if not result then return end local bufnr = api.nvim_get_current_buf() diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 80e4399fe1..49798a452f 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -621,31 +621,6 @@ function M.open_floating_preview(contents, filetype, opts) return floating_bufnr, floating_winnr end -local function validate_lsp_position(pos) - validate { pos = {pos, 't'} } - validate { - line = {pos.line, 'n'}; - character = {pos.character, 'n'}; - } - return true -end - -function M.open_floating_peek_preview(bufnr, start, finish, opts) - validate { - bufnr = {bufnr, 'n'}; - start = {start, validate_lsp_position, 'valid start Position'}; - finish = {finish, validate_lsp_position, 'valid finish Position'}; - opts = { opts, 't', true }; - } - local width = math.max(finish.character - start.character + 1, 1) - local height = math.max(finish.line - start.line + 1, 1) - local floating_winnr = api.nvim_open_win(bufnr, false, M.make_floating_popup_options(width, height, opts)) - api.nvim_win_set_cursor(floating_winnr, {start.line+1, start.character}) - api.nvim_command("autocmd CursorMoved * ++once lua pcall(vim.api.nvim_win_close, "..floating_winnr..", true)") - return floating_winnr -end - - local function highlight_range(bufnr, ns, hiname, start, finish) if start[1] == finish[1] then -- TODO care about encoding here since this is in byte index? -- cgit From 663b83814d3317fd44b59e5a0ac9732494d6e4c9 Mon Sep 17 00:00:00 2001 From: Chris Kipp Date: Sun, 26 Apr 2020 16:51:41 +0200 Subject: LSP: Add a check for null version in VersionedTextDocumentIdentifier (#12185) According to the spec there is the possibility that when a VersionedTextDocumentIdentifier is used in a TextEdit the value may be null. Currently we don't check for this and always assume that it's set. So currently if a TextEdit comes in for a rename for example with the version null, it fails as we are comparing the bufnumber with nil. https://microsoft.github.io/language-server-protocol/specification#versionedTextDocumentIdentifier --- runtime/lua/vim/lsp/util.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 49798a452f..be391c8b5b 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -159,7 +159,8 @@ end function M.apply_text_document_edit(text_document_edit) local text_document = text_document_edit.textDocument local bufnr = vim.uri_to_bufnr(text_document.uri) - if M.buf_versions[bufnr] > text_document.version then + -- `VersionedTextDocumentIdentifier`s version may be nil https://microsoft.github.io/language-server-protocol/specification#versionedTextDocumentIdentifier + if text_document.version ~= nil and M.buf_versions[bufnr] > text_document.version then print("Buffer ", text_document.uri, " newer than edits.") return end -- cgit From 50ff37308abde6c33c2800529cd58f1d7413d7b3 Mon Sep 17 00:00:00 2001 From: Mathias Fußenegger Date: Sun, 26 Apr 2020 23:56:30 +0200 Subject: LSP: Fix show_line_diagnostics #12186 Messed this up in ef0398fe88e6cc74f33fb20519997774168d7832 --- runtime/lua/vim/lsp/util.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index be391c8b5b..e45104789e 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -696,7 +696,7 @@ do local buffer_diagnostics = M.diagnostics_by_buf[bufnr] if not buffer_diagnostics then return end - local line_diagnostics = M.diagnostics_group_by_line(buffer_diagnostics[line]) + local line_diagnostics = M.diagnostics_group_by_line(buffer_diagnostics)[line] if not line_diagnostics then return end for i, diagnostic in ipairs(line_diagnostics) do @@ -707,6 +707,7 @@ do -- TODO(ashkan) make format configurable? local prefix = string.format("%d. ", i) local hiname = severity_highlights[diagnostic.severity] + assert(hiname, 'unknown severity: ' .. tostring(diagnostic.severity)) local message_lines = split_lines(diagnostic.message) table.insert(lines, prefix..message_lines[1]) table.insert(highlights, {#prefix + 1, hiname}) -- cgit From 5f41717838f4cd9d1087e452640ba554500279ab Mon Sep 17 00:00:00 2001 From: jakbyte Date: Sun, 26 Apr 2020 18:36:40 -0400 Subject: LSP: don't redefine LspDiagnostics signs #12164 fix #12162 --- runtime/lua/vim/lsp.lua | 16 ++++++++++++++++ runtime/lua/vim/lsp/util.lua | 20 +++++++++----------- 2 files changed, 25 insertions(+), 11 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 17135e078c..c7de2df25f 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -1017,5 +1017,21 @@ function lsp.get_log_path() return log.get_filename() end +local function define_default_sign(name, properties) + if not vim.fn.sign_getdefined(name) then + vim.fn.sign_define(name, properties) + end +end + +-- Define the LspDiagnostics signs if they're not defined already. +local function define_default_lsp_diagnostics_signs() + define_default_sign('LspDiagnosticsErrorSign', {text='E', texthl='LspDiagnosticsError', linehl='', numhl=''}) + define_default_sign('LspDiagnosticsWarningSign', {text='W', texthl='LspDiagnosticsWarning', linehl='', numhl=''}) + define_default_sign('LspDiagnosticsInformationSign', {text='I', texthl='LspDiagnosticsInformation', linehl='', numhl=''}) + define_default_sign('LspDiagnosticsHintSign', {text='H', texthl='LspDiagnosticsHint', linehl='', numhl=''}) +end + +define_default_lsp_diagnostics_signs() + return lsp -- vim:sw=2 ts=2 et diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index e45104789e..53e2240ff5 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -818,6 +818,7 @@ do api.nvim_buf_set_virtual_text(bufnr, diagnostic_ns, line, virt_texts, {}) end end + function M.buf_diagnostics_count(kind) local bufnr = vim.api.nvim_get_current_buf() local diagnostics = M.diagnostics_by_buf[bufnr] @@ -830,19 +831,16 @@ do end return count end - function M.buf_diagnostics_signs(bufnr, diagnostics) - vim.fn.sign_define('LspDiagnosticsErrorSign', {text=vim.g['LspDiagnosticsErrorSign'] or 'E', texthl='LspDiagnosticsError', linehl='', numhl=''}) - vim.fn.sign_define('LspDiagnosticsWarningSign', {text=vim.g['LspDiagnosticsWarningSign'] or 'W', texthl='LspDiagnosticsWarning', linehl='', numhl=''}) - vim.fn.sign_define('LspDiagnosticsInformationSign', {text=vim.g['LspDiagnosticsInformationSign'] or 'I', texthl='LspDiagnosticsInformation', linehl='', numhl=''}) - vim.fn.sign_define('LspDiagnosticsHintSign', {text=vim.g['LspDiagnosticsHintSign'] or 'H', texthl='LspDiagnosticsHint', linehl='', numhl=''}) + local diagnostic_severity_map = { + [protocol.DiagnosticSeverity.Error] = "LspDiagnosticsErrorSign"; + [protocol.DiagnosticSeverity.Warning] = "LspDiagnosticsWarningSign"; + [protocol.DiagnosticSeverity.Information] = "LspDiagnosticsInformationSign"; + [protocol.DiagnosticSeverity.Hint] = "LspDiagnosticsHintSign"; + } + + function M.buf_diagnostics_signs(bufnr, diagnostics) for _, diagnostic in ipairs(diagnostics) do - local diagnostic_severity_map = { - [protocol.DiagnosticSeverity.Error] = "LspDiagnosticsErrorSign"; - [protocol.DiagnosticSeverity.Warning] = "LspDiagnosticsWarningSign"; - [protocol.DiagnosticSeverity.Information] = "LspDiagnosticsInformationSign"; - [protocol.DiagnosticSeverity.Hint] = "LspDiagnosticsHintSign"; - } vim.fn.sign_place(0, sign_ns, diagnostic_severity_map[diagnostic.severity], bufnr, {lnum=(diagnostic.range.start.line+1)}) end end -- cgit From 4e6531ddbdce9944f9986ca5357b71171a14f05e Mon Sep 17 00:00:00 2001 From: Hirokazu Hata Date: Tue, 28 Apr 2020 23:41:39 +0900 Subject: lsp: use vim.tbl_isempty to check sign (#12190) ref: #12164 fix #12201 sign_getdefined() returns a list, {} if the sign is not defined. --- runtime/lua/vim/lsp.lua | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index c7de2df25f..c01b5d8e0c 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -1017,21 +1017,18 @@ function lsp.get_log_path() return log.get_filename() end -local function define_default_sign(name, properties) - if not vim.fn.sign_getdefined(name) then - vim.fn.sign_define(name, properties) - end -end - -- Define the LspDiagnostics signs if they're not defined already. -local function define_default_lsp_diagnostics_signs() +do + local function define_default_sign(name, properties) + if vim.tbl_isempty(vim.fn.sign_getdefined(name)) then + vim.fn.sign_define(name, properties) + end + end define_default_sign('LspDiagnosticsErrorSign', {text='E', texthl='LspDiagnosticsError', linehl='', numhl=''}) define_default_sign('LspDiagnosticsWarningSign', {text='W', texthl='LspDiagnosticsWarning', linehl='', numhl=''}) define_default_sign('LspDiagnosticsInformationSign', {text='I', texthl='LspDiagnosticsInformation', linehl='', numhl=''}) define_default_sign('LspDiagnosticsHintSign', {text='H', texthl='LspDiagnosticsHint', linehl='', numhl=''}) end -define_default_lsp_diagnostics_signs() - return lsp -- vim:sw=2 ts=2 et -- cgit From e9cc383614d449b7269632c991525db77c387154 Mon Sep 17 00:00:00 2001 From: Yen3 Date: Tue, 28 Apr 2020 16:47:22 +0200 Subject: LSP: support tagstack #12096 --- runtime/lua/vim/lsp/util.lua | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 53e2240ff5..9ca18cb2b1 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -421,7 +421,13 @@ function M.jump_to_location(location) local bufnr = vim.uri_to_bufnr(location.uri) -- Save position in jumplist vim.cmd "normal! m'" - -- TODO(ashkan) use tagfunc here to update tagstack. + + -- Push a new item into tagstack + local items = {} + table.insert(items, {tagname=vim.fn.expand(""), from=vim.fn.getpos('.')}) + vim.fn.settagstack(vim.fn.bufnr('%'), {items=items}, 't') + + --- Jump to new location api.nvim_set_current_buf(bufnr) local row = location.range.start.line local col = location.range.start.character -- cgit From e4a1be779b9a6bb9a47700ebf92f8dd934bb1712 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Wed, 29 Apr 2020 10:32:34 +0900 Subject: lsp/completion: Expose completion_item under completed_items.user_data. By passing through completion_item it's now possible for snippet plugins to add LSP snippet support. --- runtime/lua/vim/lsp/util.lua | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 53e2240ff5..9a51bc2557 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -240,6 +240,13 @@ function M.text_document_completion_list_to_complete_items(result, prefix) icase = 1, dup = 1, empty = 1, + user_data = { + nvim = { + lsp = { + completion_item = completion_item + } + } + }, }) end -- cgit From f9055c585f597fe4ea8ecb990927a2826234393c Mon Sep 17 00:00:00 2001 From: Ghjuvan Lacambre Date: Wed, 29 Apr 2020 16:53:13 +0200 Subject: LSP: enable using different highlighting rules for LSP signs (#12176) This commit creates 4 new highlight groups: - LspDiagnosticsErrorSign - LspDiagnosticsWarningSign - LspDiagnosticsInformationSign - LspDiagnosticsHintSign These highlight groups are linked to their corresponding LspDiagnostics highlight groups by default. This lets users choose a different color for their sign columns and virtualtext diagnostics. --- runtime/lua/vim/lsp.lua | 8 ++++---- runtime/lua/vim/lsp/util.lua | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index c01b5d8e0c..4c1c52c796 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -1024,10 +1024,10 @@ do vim.fn.sign_define(name, properties) end end - define_default_sign('LspDiagnosticsErrorSign', {text='E', texthl='LspDiagnosticsError', linehl='', numhl=''}) - define_default_sign('LspDiagnosticsWarningSign', {text='W', texthl='LspDiagnosticsWarning', linehl='', numhl=''}) - define_default_sign('LspDiagnosticsInformationSign', {text='I', texthl='LspDiagnosticsInformation', linehl='', numhl=''}) - define_default_sign('LspDiagnosticsHintSign', {text='H', texthl='LspDiagnosticsHint', linehl='', numhl=''}) + define_default_sign('LspDiagnosticsErrorSign', {text='E', texthl='LspDiagnosticsErrorSign', linehl='', numhl=''}) + define_default_sign('LspDiagnosticsWarningSign', {text='W', texthl='LspDiagnosticsWarningSign', linehl='', numhl=''}) + define_default_sign('LspDiagnosticsInformationSign', {text='I', texthl='LspDiagnosticsInformationSign', linehl='', numhl=''}) + define_default_sign('LspDiagnosticsHintSign', {text='H', texthl='LspDiagnosticsHintSign', linehl='', numhl=''}) end return lsp diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 9ca18cb2b1..7d4dc072e5 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -672,6 +672,7 @@ do table.insert(cmd_parts, k.."="..v) end api.nvim_command(table.concat(cmd_parts, ' ')) + api.nvim_command('highlight link ' .. highlight_name .. 'Sign ' .. highlight_name) severity_highlights[severity] = highlight_name end -- cgit From e5022c61ed769153dab5a91752c52a8f9ad3b504 Mon Sep 17 00:00:00 2001 From: Thomas Vigouroux <39092278+vigoux@users.noreply.github.com> Date: Fri, 1 May 2020 07:43:30 +0200 Subject: treesitter: unknown predicates always match #12173 --- runtime/lua/vim/treesitter.lua | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua index 1836227540..d3b78a7f73 100644 --- a/runtime/lua/vim/treesitter.lua +++ b/runtime/lua/vim/treesitter.lua @@ -159,6 +159,9 @@ function Query:match_preds(match, pattern, bufnr) end local regexes = self.regexes[pattern] for i, pred in pairs(preds) do + -- Here we only want to return if a predicate DOES NOT match, and + -- continue on the other case. This way unknown predicates will not be considered, + -- which allows some testing and easier user extensibility (#12173). if pred[1] == "eq?" then local node = match[pred[2]] local node_text = get_node_text(node, bufnr) @@ -184,9 +187,9 @@ function Query:match_preds(match, pattern, bufnr) if start_row ~= end_row then return false end - return regexes[i]:match_line(bufnr, start_row, start_col, end_col) - else - return false + if not regexes[i]:match_line(bufnr, start_row, start_col, end_col) then + return false + end end end return true -- cgit From 6dc8398944fd86038b07d77fcab92cd282555dee Mon Sep 17 00:00:00 2001 From: ckipp01 Date: Mon, 27 Apr 2020 10:55:06 +0200 Subject: [LSP] check for vim.NIL and add apply_text_document_edit tests --- runtime/lua/vim/lsp/util.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 68f3b35df3..82b9a0b3aa 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -160,7 +160,7 @@ function M.apply_text_document_edit(text_document_edit) local text_document = text_document_edit.textDocument local bufnr = vim.uri_to_bufnr(text_document.uri) -- `VersionedTextDocumentIdentifier`s version may be nil https://microsoft.github.io/language-server-protocol/specification#versionedTextDocumentIdentifier - if text_document.version ~= nil and M.buf_versions[bufnr] > text_document.version then + if text_document.version ~= vim.NIL and M.buf_versions[bufnr] > text_document.version then print("Buffer ", text_document.uri, " newer than edits.") return end -- cgit From d0af0f5c9e12e0d9070c0709412eb9fd55534295 Mon Sep 17 00:00:00 2001 From: Hirokazu Hata Date: Sat, 2 May 2020 15:08:52 +0900 Subject: lsp: fix lsp.util.symbols_to_items fix: https://github.com/neovim/neovim/pull/11931#issuecomment-622422581 There was an error in the process of flattening the hierarchical structure. So when DocumentSymbol has children, our client can't handle it correctly. --- runtime/lua/vim/lsp/util.lua | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 68f3b35df3..bd4f55846c 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -956,10 +956,8 @@ function M.symbols_to_items(symbols, bufnr) text = '['..kind..'] '..symbol.name }) if symbol.children then - for _, child in ipairs(symbol) do - for _, v in ipairs(_symbols_to_items(child, _items, _bufnr)) do - vim.list_extend(_items, v) - end + for _, v in ipairs(_symbols_to_items(symbol.children, _items, _bufnr)) do + vim.list_extend(_items, v) end end end -- cgit From 2f42e4d0c86163e64eb56077939fe405dc434e42 Mon Sep 17 00:00:00 2001 From: Christian Clason Date: Sat, 2 May 2020 15:21:07 +0200 Subject: LSP: Support LocationLink (#12231) * support LocationLink in callbacks * announce linkSupport in client capabilities --- runtime/lua/vim/lsp/protocol.lua | 12 ++++++++++++ runtime/lua/vim/lsp/util.lua | 19 ++++++++++++------- 2 files changed, 24 insertions(+), 7 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index 41e8119c8c..bca4840674 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -644,6 +644,18 @@ function protocol.make_client_capabilities() -- TODO(tjdevries): Implement this contextSupport = false; }; + declaration = { + linkSupport = true; + }; + definition = { + linkSupport = true; + }; + implementation = { + linkSupport = true; + }; + typeDefinition = { + linkSupport = true; + }; hover = { dynamicRegistration = false; contentFormat = { protocol.MarkupKind.Markdown; protocol.MarkupKind.PlainText }; diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 68f3b35df3..9f1275b43c 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -424,8 +424,10 @@ function M.make_floating_popup_options(width, height, opts) end function M.jump_to_location(location) - if location.uri == nil then return end - local bufnr = vim.uri_to_bufnr(location.uri) + -- location may be Location or LocationLink + local uri = location.uri or location.targetUri + if uri == nil then return end + local bufnr = vim.uri_to_bufnr(uri) -- Save position in jumplist vim.cmd "normal! m'" @@ -436,8 +438,9 @@ function M.jump_to_location(location) --- Jump to new location api.nvim_set_current_buf(bufnr) - local row = location.range.start.line - local col = location.range.start.character + local range = location.range or location.targetSelectionRange + local row = range.start.line + local col = range.start.character local line = api.nvim_buf_get_lines(0, row, row+1, true)[1] col = vim.str_byteindex(line, col) api.nvim_win_set_cursor(0, {row + 1, col}) @@ -876,9 +879,11 @@ function M.locations_to_items(locations) end; }) for _, d in ipairs(locations) do - local start = d.range.start - local fname = assert(vim.uri_to_fname(d.uri)) - table.insert(grouped[fname], {start = start}) + -- locations may be Location or LocationLink + local uri = d.uri or d.targetUri + local fname = assert(vim.uri_to_fname(uri)) + local range = d.range or d.targetSelectionRange + table.insert(grouped[fname], {start = range.start}) end -- cgit From ea347b18d883999ed9a8d2e7c00068058135232f Mon Sep 17 00:00:00 2001 From: Christian Clason Date: Sat, 2 May 2020 17:56:05 +0200 Subject: lsp: add workspace/symbol (#12224) * lsp: add workspace/symbol * refactor symbol callback * set hierarchical symbol support to true * add documentation and default mapping Co-authored-by: Hirokazu Hata --- runtime/lua/vim/lsp.lua | 1 + runtime/lua/vim/lsp/buf.lua | 6 ++++++ runtime/lua/vim/lsp/callbacks.lua | 4 +++- runtime/lua/vim/lsp/protocol.lua | 13 +++++++++++++ 4 files changed, 23 insertions(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 4c1c52c796..61da2130c8 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -533,6 +533,7 @@ function lsp.start_client(config) or (not client.resolved_capabilities.goto_definition and method == 'textDocument/definition') or (not client.resolved_capabilities.implementation and method == 'textDocument/implementation') or (not client.resolved_capabilities.document_symbol and method == 'textDocument/documentSymbol') + or (not client.resolved_capabilities.workspace_symbol and method == 'textDocument/workspaceSymbol') then callback(unsupported_method(method), method, nil, client_id, bufnr) return diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 587d1f52e9..0b45951a56 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -137,6 +137,12 @@ function M.document_symbol() request('textDocument/documentSymbol', params) end +function M.workspace_symbol(query) + query = query or npcall(vfn.input, "Query: ") + local params = {query = query} + request('workspace/symbol', params) +end + --- Send request to server to resolve document highlights for the --- current text document position. This request can be associated --- to key mapping or to events such as `CursorHold`, eg: diff --git a/runtime/lua/vim/lsp/callbacks.lua b/runtime/lua/vim/lsp/callbacks.lua index bd2cbf1ea7..70d21be8e7 100644 --- a/runtime/lua/vim/lsp/callbacks.lua +++ b/runtime/lua/vim/lsp/callbacks.lua @@ -54,13 +54,15 @@ M['textDocument/references'] = function(_, _, result) api.nvim_command("wincmd p") end -M['textDocument/documentSymbol'] = function(_, _, result, _, bufnr) +local symbol_callback = function(_, _, result, _, bufnr) if not result or vim.tbl_isempty(result) then return end util.set_qflist(util.symbols_to_items(result, bufnr)) api.nvim_command("copen") api.nvim_command("wincmd p") end +M['textDocument/documentSymbol'] = symbol_callback +M['workspace/symbol'] = symbol_callback M['textDocument/rename'] = function(_, _, result) if not result then return end diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index bca4840674..1c499d23a6 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -688,6 +688,19 @@ function protocol.make_client_capabilities() }; hierarchicalDocumentSymbolSupport = true; }; + workspaceSymbol = { + dynamicRegistration = false; + symbolKind = { + valueSet = (function() + local res = {} + for k in pairs(protocol.SymbolKind) do + if type(k) == 'number' then table.insert(res, k) end + end + return res + end)(); + }; + hierarchicalWorkspaceSymbolSupport = true; + }; }; workspace = nil; experimental = nil; -- cgit From 501ef952983dba09b160d2e910d3842364502d7c Mon Sep 17 00:00:00 2001 From: Christian Clason Date: Sun, 3 May 2020 18:01:04 +0200 Subject: lsp: fixup workspace symbol capabilities (#12233) use workspace.symbol instead of workspaceSymbol to mimic the lsp spec. --- runtime/lua/vim/lsp/protocol.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index 1c499d23a6..76817e3a4a 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -688,7 +688,9 @@ function protocol.make_client_capabilities() }; hierarchicalDocumentSymbolSupport = true; }; - workspaceSymbol = { + }; + workspace = { + symbol = { dynamicRegistration = false; symbolKind = { valueSet = (function() @@ -702,7 +704,6 @@ function protocol.make_client_capabilities() hierarchicalWorkspaceSymbolSupport = true; }; }; - workspace = nil; experimental = nil; } end -- cgit From 2c40a38b39a944a3e1a90302c1061b4e6e3ba6ac Mon Sep 17 00:00:00 2001 From: Mathias Fußenegger Date: Tue, 5 May 2020 05:06:40 +0200 Subject: LSP: Avoid URI-to-fname conversion for non-file URIs #12243 Fixes https://github.com/neovim/neovim/issues/12210 --- runtime/lua/vim/uri.lua | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/uri.lua b/runtime/lua/vim/uri.lua index d91fb7ffd3..e28cc9e20f 100644 --- a/runtime/lua/vim/uri.lua +++ b/runtime/lua/vim/uri.lua @@ -66,7 +66,13 @@ local function uri_from_fname(path) end local function uri_from_bufnr(bufnr) - return uri_from_fname(vim.api.nvim_buf_get_name(bufnr)) + local fname = vim.api.nvim_buf_get_name(bufnr) + local scheme = fname:match("^([a-z]+)://.*") + if scheme then + return fname + else + return uri_from_fname(fname) + end end local function uri_to_fname(uri) @@ -83,7 +89,12 @@ end -- Return or create a buffer for a uri. local function uri_to_bufnr(uri) - return vim.fn.bufadd((uri_to_fname(uri))) + local scheme = assert(uri:match("^([a-z]+)://.*"), 'Uri must contain a scheme: ' .. uri) + if scheme == 'file' then + return vim.fn.bufadd(uri_to_fname(uri)) + else + return vim.fn.bufadd(uri) + end end return { -- cgit From f605eeec65a3f4923e02f97fcef713365cd2e588 Mon Sep 17 00:00:00 2001 From: Gabriel Sanches Date: Tue, 5 May 2020 00:12:35 -0300 Subject: lsp: fix tagstack for location jump #12248 --- runtime/lua/vim/lsp/util.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 6a1e799489..494eebf9ea 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -432,9 +432,9 @@ function M.jump_to_location(location) vim.cmd "normal! m'" -- Push a new item into tagstack - local items = {} - table.insert(items, {tagname=vim.fn.expand(""), from=vim.fn.getpos('.')}) - vim.fn.settagstack(vim.fn.bufnr('%'), {items=items}, 't') + local from = {vim.fn.bufnr('%'), vim.fn.line('.'), vim.fn.col('.'), 0} + local items = {{tagname=vim.fn.expand(''), from=from}} + vim.fn.settagstack(vim.fn.win_getid(), {items=items}, 't') --- Jump to new location api.nvim_set_current_buf(bufnr) -- cgit From 7432b3ca4ca6a5f8389c79fd17f20de22fc8e54b Mon Sep 17 00:00:00 2001 From: Gabriel Sanches Date: Thu, 7 May 2020 10:30:42 -0300 Subject: lsp: set buflisted when jumping to location (#12253) --- runtime/lua/vim/lsp/util.lua | 1 + 1 file changed, 1 insertion(+) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 494eebf9ea..eaa5f0f02f 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -438,6 +438,7 @@ function M.jump_to_location(location) --- Jump to new location api.nvim_set_current_buf(bufnr) + api.nvim_buf_set_option(0, 'buflisted', true) local range = location.range or location.targetSelectionRange local row = range.start.line local col = range.start.character -- cgit From 9a67b030d9a054648296b45b615684dee768582d Mon Sep 17 00:00:00 2001 From: Hirokazu Hata Date: Fri, 8 May 2020 05:23:25 +0900 Subject: lsp: Handle unknown CompletionItemKind and SymbolKind (#12257) * lsp: handle kinds not specified in protocol fix: #12200 If the client set "symbolKind.valueSet", the client must handle it properly even if it receives a value outside the specification. * test: add lsp.util.{get_completion_item_kind_name, get_symbol_kind_name} test case * lsp: make lsp.util.{get_completion_item_kind_name, get_symbol_kind_name} private --- runtime/lua/vim/lsp/util.lua | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index eaa5f0f02f..4d4762dac8 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -203,6 +203,13 @@ local function remove_unmatch_completion_items(items, prefix) end, items) end +-- Acording to LSP spec, if the client set "completionItemKind.valueSet", +-- the client must handle it properly even if it receives a value outside the specification. +-- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion +function M._get_completion_item_kind_name(completion_item_kind) + return protocol.CompletionItemKind[completion_item_kind] or "Unknown" +end + --- Getting vim complete-items with incomplete flag. -- @params CompletionItem[], CompletionList or nil (https://microsoft.github.io/language-server-protocol/specification#textDocument_completion) -- @return { matches = complete-items table, incomplete = boolean } @@ -234,7 +241,7 @@ function M.text_document_completion_list_to_complete_items(result, prefix) table.insert(matches, { word = word, abbr = completion_item.label, - kind = protocol.CompletionItemKind[completion_item.kind] or '', + kind = M._get_completion_item_kind_name(completion_item.kind), menu = completion_item.detail or '', info = info, icase = 1, @@ -935,6 +942,13 @@ function M.set_qflist(items) }) end +-- Acording to LSP spec, if the client set "symbolKind.valueSet", +-- the client must handle it properly even if it receives a value outside the specification. +-- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentSymbol +function M._get_symbol_kind_name(symbol_kind) + return protocol.SymbolKind[symbol_kind] or "Unknown" +end + --- Convert symbols to quickfix list items --- --@symbols DocumentSymbol[] or SymbolInformation[] @@ -943,7 +957,7 @@ function M.symbols_to_items(symbols, bufnr) for _, symbol in ipairs(_symbols) do if symbol.location then -- SymbolInformation type local range = symbol.location.range - local kind = protocol.SymbolKind[symbol.kind] + local kind = M._get_symbol_kind_name(symbol.kind) table.insert(_items, { filename = vim.uri_to_fname(symbol.location.uri), lnum = range.start.line + 1, @@ -952,7 +966,7 @@ function M.symbols_to_items(symbols, bufnr) text = '['..kind..'] '..symbol.name, }) elseif symbol.range then -- DocumentSymbole type - local kind = protocol.SymbolKind[symbol.kind] + local kind = M._get_symbol_kind_name(symbol.kind) table.insert(_items, { -- bufnr = _bufnr, filename = vim.api.nvim_buf_get_name(_bufnr), -- cgit From 281e44f7bb24866d4a580d32aaeab8c8033f1fb0 Mon Sep 17 00:00:00 2001 From: Christian Clason Date: Fri, 8 May 2020 16:04:41 +0200 Subject: lsp: Make apply_text_edits non-ASCII safe (#12223) * Make apply_text_edits non-ASCII safe Use `vim.str_byteindex` to correct starting and ending positions for text edits if the line contains non-ASCII characters. Fixes #12221 * text_edit may be applied to other buffers * make sure the buffer is loaded * add comments * add test for non-ASCII edits --- runtime/lua/vim/lsp/util.lua | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 4d4762dac8..9efab73c2b 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -96,16 +96,28 @@ end) function M.apply_text_edits(text_edits, bufnr) if not next(text_edits) then return end + if not api.nvim_buf_is_loaded(bufnr) then + vim.fn.bufload(bufnr) + end local start_line, finish_line = math.huge, -1 local cleaned = {} for i, e in ipairs(text_edits) do + -- adjust start and end column for UTF-16 encoding of non-ASCII characters + local start_row = e.range.start.line + local start_col = e.range.start.character + local start_bline = api.nvim_buf_get_lines(bufnr, start_row, start_row+1, true)[1] + start_col = vim.str_byteindex(start_bline, start_col) + local end_row = e.range["end"].line + local end_col = e.range["end"].character + local end_bline = api.nvim_buf_get_lines(bufnr, end_row, end_row+1, true)[1] + end_col = vim.str_byteindex(end_bline, end_col) start_line = math.min(e.range.start.line, start_line) finish_line = math.max(e.range["end"].line, finish_line) -- TODO(ashkan) sanity check ranges for overlap. table.insert(cleaned, { i = i; - A = {e.range.start.line; e.range.start.character}; - B = {e.range["end"].line; e.range["end"].character}; + A = {start_row; start_col}; + B = {end_row; end_col}; lines = vim.split(e.newText, '\n', true); }) end @@ -113,9 +125,6 @@ function M.apply_text_edits(text_edits, bufnr) -- Reverse sort the orders so we can apply them without interfering with -- eachother. Also add i as a sort key to mimic a stable sort. table.sort(cleaned, edit_sort_key) - if not api.nvim_buf_is_loaded(bufnr) then - vim.fn.bufload(bufnr) - end local lines = api.nvim_buf_get_lines(bufnr, start_line, finish_line + 1, false) local fix_eol = api.nvim_buf_get_option(bufnr, 'fixeol') local set_eol = fix_eol and api.nvim_buf_line_count(bufnr) <= finish_line + 1 @@ -443,7 +452,7 @@ function M.jump_to_location(location) local items = {{tagname=vim.fn.expand(''), from=from}} vim.fn.settagstack(vim.fn.win_getid(), {items=items}, 't') - --- Jump to new location + --- Jump to new location (adjusting for UTF-16 encoding of characters) api.nvim_set_current_buf(bufnr) api.nvim_buf_set_option(0, 'buflisted', true) local range = location.range or location.targetSelectionRange -- cgit From 55b62a937c27715f7e6c299ae312c38edf08fa74 Mon Sep 17 00:00:00 2001 From: Mathias Fußenegger Date: Mon, 11 May 2020 16:56:35 +0200 Subject: LSP: Make applyEdit return a response (#12270) According to the specification workspace/applyEdit needs to respond with a `ApplyWorkspaceEditResponse` See https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit This is a subset of https://github.com/neovim/neovim/pull/11607 --- runtime/lua/vim/lsp/callbacks.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/callbacks.lua b/runtime/lua/vim/lsp/callbacks.lua index 70d21be8e7..09ca4b61e4 100644 --- a/runtime/lua/vim/lsp/callbacks.lua +++ b/runtime/lua/vim/lsp/callbacks.lua @@ -17,7 +17,11 @@ M['workspace/applyEdit'] = function(_, _, workspace_edit) if workspace_edit.label then print("Workspace edit", workspace_edit.label) end - util.apply_workspace_edit(workspace_edit.edit) + local status, result = pcall(util.apply_workspace_edit, workspace_edit.edit) + return { + applied = status; + failureReason = result; + } end M['textDocument/publishDiagnostics'] = function(_, _, result) -- cgit From 02155f5c102539d5052912956606a452a78ad78c Mon Sep 17 00:00:00 2001 From: landerlo Date: Thu, 14 May 2020 04:14:52 +0100 Subject: lsp: fix bug when documentEdit version=null for unattached buffer (#12272) --- runtime/lua/vim/lsp/util.lua | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 9efab73c2b..099a77099b 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -168,10 +168,12 @@ end function M.apply_text_document_edit(text_document_edit) local text_document = text_document_edit.textDocument local bufnr = vim.uri_to_bufnr(text_document.uri) - -- `VersionedTextDocumentIdentifier`s version may be nil https://microsoft.github.io/language-server-protocol/specification#versionedTextDocumentIdentifier - if text_document.version ~= vim.NIL and M.buf_versions[bufnr] > text_document.version then - print("Buffer ", text_document.uri, " newer than edits.") - return + if text_document.version then + -- `VersionedTextDocumentIdentifier`s version may be null https://microsoft.github.io/language-server-protocol/specification#versionedTextDocumentIdentifier + if text_document.version ~= vim.NIL and M.buf_versions[bufnr] ~= nil and M.buf_versions[bufnr] > text_document.version then + print("Buffer ", text_document.uri, " newer than edits.") + return + end end M.apply_text_edits(text_document_edit.edits, bufnr) end -- cgit From da6f38ab3cd87615e212b83ff9bbb9d585e4768e Mon Sep 17 00:00:00 2001 From: Mathias Fußenegger Date: Thu, 14 May 2020 18:56:33 +0200 Subject: LSP: Add workspace.applyEdit client capabilities (#12313) applyEdit is supported by the built-in client. --- runtime/lua/vim/lsp/protocol.lua | 1 + 1 file changed, 1 insertion(+) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index 76817e3a4a..ee6e29bac0 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -703,6 +703,7 @@ function protocol.make_client_capabilities() }; hierarchicalWorkspaceSymbolSupport = true; }; + applyEdit = true; }; experimental = nil; } -- cgit From f559e5249e3aa155687b335272da8f0c73255ee4 Mon Sep 17 00:00:00 2001 From: Jesse Date: Sat, 16 May 2020 01:18:59 +0200 Subject: LSP: Add textDocument/codeAction support (#11607) * Add textDocument/codeAction * Add callback for workspace/executeCommand * Escape newlines in codeAction titles * Return empty list in get_line_diagnostics if no buffer diagnostics * Add stub documentation * Validate context parameter in code_action * Add support for edit in CodeAction responses * Group diagnostics by line in vim.lsp.util.get_line_diagnostics() * Advertise code action literal support --- runtime/lua/vim/lsp/buf.lua | 16 ++++++++++++++++ runtime/lua/vim/lsp/callbacks.lua | 40 +++++++++++++++++++++++++++++++++++++++ runtime/lua/vim/lsp/protocol.lua | 9 +++++++++ runtime/lua/vim/lsp/util.lua | 37 ++++++++++++++++++++++++++++-------- 4 files changed, 94 insertions(+), 8 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 0b45951a56..7a819f3c3d 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -161,5 +161,21 @@ function M.clear_references() util.buf_clear_references() end +function M.code_action(context) + validate { context = { context, 't', true } } + context = context or { diagnostics = util.get_line_diagnostics() } + local params = util.make_range_params() + params.context = context + request('textDocument/codeAction', params) +end + +function M.execute_command(command) + validate { + command = { command.command, 's' }, + arguments = { command.arguments, 't', true } + } + request('workspace/executeCommand', command) +end + return M -- vim:sw=2 ts=2 et diff --git a/runtime/lua/vim/lsp/callbacks.lua b/runtime/lua/vim/lsp/callbacks.lua index 09ca4b61e4..17f88a5181 100644 --- a/runtime/lua/vim/lsp/callbacks.lua +++ b/runtime/lua/vim/lsp/callbacks.lua @@ -3,6 +3,7 @@ local protocol = require 'vim.lsp.protocol' local util = require 'vim.lsp.util' local vim = vim local api = vim.api +local buf = require 'vim.lsp.buf' local M = {} @@ -11,6 +12,45 @@ local function err_message(...) api.nvim_command("redraw") end +M['workspace/executeCommand'] = function(err, _) + if err then + error("Could not execute code action: "..err.message) + end +end + +M['textDocument/codeAction'] = function(_, _, actions) + if vim.tbl_isempty(actions) then + print("No code actions available") + return + end + + local option_strings = {"Code Actions:"} + for i, action in ipairs(actions) do + local title = action.title:gsub('\r\n', '\\r\\n') + title = title:gsub('\n', '\\n') + table.insert(option_strings, string.format("%d. %s", i, title)) + end + + local choice = vim.fn.inputlist(option_strings) + if choice < 1 or choice > #actions then + return + end + local action_chosen = actions[choice] + -- textDocument/codeAction can return either Command[] or CodeAction[]. + -- If it is a CodeAction, it can have either an edit, a command or both. + -- Edits should be executed first + if action_chosen.edit or type(action_chosen.command) == "table" then + if action_chosen.edit then + util.apply_workspace_edit(action_chosen.edit) + end + if type(action_chosen.command) == "table" then + buf.execute_command(action_chosen.command) + end + else + buf.execute_command(action_chosen) + end +end + M['workspace/applyEdit'] = function(_, _, workspace_edit) if not workspace_edit then return end -- TODO(ashkan) Do something more with label? diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index ee6e29bac0..877d11411b 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -620,6 +620,15 @@ function protocol.make_client_capabilities() -- Send textDocument/didSave after saving (BufWritePost) didSave = true; }; + codeAction = { + dynamicRegistration = false; + + codeActionLiteralSupport = { + codeActionKind = { + valueSet = {}; + }; + }; + }; completion = { dynamicRegistration = false; completionItem = { diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 099a77099b..c92a317d0c 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -720,19 +720,28 @@ do return severity_highlights[severity] end - function M.show_line_diagnostics() + function M.get_line_diagnostics() local bufnr = api.nvim_get_current_buf() - local line = api.nvim_win_get_cursor(0)[1] - 1 + local linenr = api.nvim_win_get_cursor(0)[1] - 1 + + local buffer_diagnostics = M.diagnostics_by_buf[bufnr] + + if not buffer_diagnostics then + return {} + end + + local diagnostics_by_line = M.diagnostics_group_by_line(buffer_diagnostics) + return diagnostics_by_line[linenr] or {} + end + + function M.show_line_diagnostics() -- local marks = api.nvim_buf_get_extmarks(bufnr, diagnostic_ns, {line, 0}, {line, -1}, {}) -- if #marks == 0 then -- return -- end local lines = {"Diagnostics:"} local highlights = {{0, "Bold"}} - - local buffer_diagnostics = M.diagnostics_by_buf[bufnr] - if not buffer_diagnostics then return end - local line_diagnostics = M.diagnostics_group_by_line(buffer_diagnostics)[line] + local line_diagnostics = M.get_line_diagnostics() if not line_diagnostics then return end for i, diagnostic in ipairs(line_diagnostics) do @@ -1044,14 +1053,26 @@ function M.try_trim_markdown_code_blocks(lines) end local str_utfindex = vim.str_utfindex -function M.make_position_params() +local function make_position_param() local row, col = unpack(api.nvim_win_get_cursor(0)) row = row - 1 local line = api.nvim_buf_get_lines(0, row, row+1, true)[1] col = str_utfindex(line, col) + return { line = row; character = col; } +end + +function M.make_position_params() return { textDocument = M.make_text_document_params(); - position = { line = row; character = col; } + position = make_position_param() + } +end + +function M.make_range_params() + local position = make_position_param() + return { + textDocument = { uri = vim.uri_from_bufnr(0) }, + range = { start = position; ["end"] = position; } } end -- cgit From 986bed23294d000565ceb04274a6c645b7c987c9 Mon Sep 17 00:00:00 2001 From: Andy Lindeman Date: Fri, 15 May 2020 21:34:28 -0400 Subject: Check for nil before checking for empty table At least the `gopls` language server seems to return nil/null if no code actions are available. Currently this results in an error: > Error executing vim.schedule lua callback: shared.lua:199: Expected table, got nil --- runtime/lua/vim/lsp/callbacks.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/callbacks.lua b/runtime/lua/vim/lsp/callbacks.lua index 17f88a5181..37e9f1e5c1 100644 --- a/runtime/lua/vim/lsp/callbacks.lua +++ b/runtime/lua/vim/lsp/callbacks.lua @@ -19,7 +19,7 @@ M['workspace/executeCommand'] = function(err, _) end M['textDocument/codeAction'] = function(_, _, actions) - if vim.tbl_isempty(actions) then + if actions == nil or vim.tbl_isempty(actions) then print("No code actions available") return end -- cgit From ae5bd0454ee4ed3bdbf22e953a216449ca34dd46 Mon Sep 17 00:00:00 2001 From: Hirokazu Hata Date: Mon, 18 May 2020 02:24:34 +0900 Subject: lua: add tbl_deep_extend (#11969) --- runtime/lua/vim/shared.lua | 41 ++++++++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 11 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index d18fcfaf95..2135bfc837 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -200,16 +200,7 @@ function vim.tbl_isempty(t) return next(t) == nil end ---- Merges two or more map-like tables. ---- ---@see |extend()| ---- ---@param behavior Decides what to do if a key is found in more than one map: ---- - "error": raise an error ---- - "keep": use value from the leftmost map ---- - "force": use value from the rightmost map ---@param ... Two or more map-like tables. -function vim.tbl_extend(behavior, ...) +local function tbl_extend(behavior, deep_extend, ...) if (behavior ~= 'error' and behavior ~= 'keep' and behavior ~= 'force') then error('invalid "behavior": '..tostring(behavior)) end @@ -228,7 +219,9 @@ function vim.tbl_extend(behavior, ...) vim.validate{["after the second argument"] = {tbl,'t'}} if tbl then for k, v in pairs(tbl) do - if behavior ~= 'force' and ret[k] ~= nil then + if type(v) == 'table' and deep_extend and not vim.tbl_islist(v) then + ret[k] = tbl_extend(behavior, true, ret[k] or vim.empty_dict(), v) + elseif behavior ~= 'force' and ret[k] ~= nil then if behavior == 'error' then error('key found in more than one map: '..k) end -- Else behavior is "keep". @@ -241,6 +234,32 @@ function vim.tbl_extend(behavior, ...) return ret end +--- Merges two or more map-like tables. +--- +--@see |extend()| +--- +--@param behavior Decides what to do if a key is found in more than one map: +--- - "error": raise an error +--- - "keep": use value from the leftmost map +--- - "force": use value from the rightmost map +--@param ... Two or more map-like tables. +function vim.tbl_extend(behavior, ...) + return tbl_extend(behavior, false, ...) +end + +--- Merges recursively two or more map-like tables. +--- +--@see |tbl_extend()| +--- +--@param behavior Decides what to do if a key is found in more than one map: +--- - "error": raise an error +--- - "keep": use value from the leftmost map +--- - "force": use value from the rightmost map +--@param ... Two or more map-like tables. +function vim.tbl_deep_extend(behavior, ...) + return tbl_extend(behavior, true, ...) +end + --- Deep compare values for equality function vim.deep_equal(a, b) if a == b then return true end -- cgit From 4fbbe1c957509393563be9492d03b9e95bb08e6a Mon Sep 17 00:00:00 2001 From: Andreas Johansson Date: Sun, 17 May 2020 19:47:14 +0200 Subject: lsp: Handle end lines in apply_text_edits (#12314) If the LSP sends an end line that is larger than what nvim considers to be the last line, you get an Index out of bounds error when fetching the line from nvim, a change that was introduced in #12223. This change removes the strict indexing and checks the return value from nvim_buf_get_lines. --- runtime/lua/vim/lsp/util.lua | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index c92a317d0c..534c90f1fd 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -94,6 +94,19 @@ local edit_sort_key = sort_by_key(function(e) return {e.A[1], e.A[2], e.i} end) +local function get_line_byte_from_line_character(bufnr, lnum, cnum) + -- Skip check when the byte and character position is the same + if cnum > 0 then + local lines = api.nvim_buf_get_lines(bufnr, lnum, lnum+1, false) + + if #lines > 0 then + return vim.str_byteindex(lines[1], cnum) + end + end + + return cnum +end + function M.apply_text_edits(text_edits, bufnr) if not next(text_edits) then return end if not api.nvim_buf_is_loaded(bufnr) then @@ -104,13 +117,15 @@ function M.apply_text_edits(text_edits, bufnr) for i, e in ipairs(text_edits) do -- adjust start and end column for UTF-16 encoding of non-ASCII characters local start_row = e.range.start.line - local start_col = e.range.start.character - local start_bline = api.nvim_buf_get_lines(bufnr, start_row, start_row+1, true)[1] - start_col = vim.str_byteindex(start_bline, start_col) + local start_col = get_line_byte_from_line_character( + bufnr, + start_row, + e.range.start.character) local end_row = e.range["end"].line - local end_col = e.range["end"].character - local end_bline = api.nvim_buf_get_lines(bufnr, end_row, end_row+1, true)[1] - end_col = vim.str_byteindex(end_bline, end_col) + local end_col = get_line_byte_from_line_character( + bufnr, + end_row, + e.range["end"].character) start_line = math.min(e.range.start.line, start_line) finish_line = math.max(e.range["end"].line, finish_line) -- TODO(ashkan) sanity check ranges for overlap. -- cgit From a91ce497b4f4d6c68e3009e5219d6b2ae0f63f7f Mon Sep 17 00:00:00 2001 From: Eisuke Kawashima Date: Mon, 18 May 2020 09:12:22 +0900 Subject: lsp: Fix timezone format of LSP log (ISO 8601) (#12332) --- runtime/lua/vim/lsp/log.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/log.lua b/runtime/lua/vim/lsp/log.lua index c0db5e5485..78aabf08ce 100644 --- a/runtime/lua/vim/lsp/log.lua +++ b/runtime/lua/vim/lsp/log.lua @@ -17,7 +17,7 @@ log.levels = { -- Default log level is warn. local current_log_level = log.levels.WARN -local log_date_format = "%FT%H:%M:%SZ%z" +local log_date_format = "%FT%H:%M:%S%z" do local path_sep = vim.loop.os_uname().sysname == "Windows" and "\\" or "/" -- cgit From f2894bffb024b712e69158d7914e9d9d3d495f72 Mon Sep 17 00:00:00 2001 From: Christian Clason Date: Mon, 18 May 2020 15:49:50 +0200 Subject: lua: Add highlight.on_yank (#12279) * add lua function to highlight yanked region * extract namespace, better naming, default values * add default for event argument * free timer * factor out mark to position calculation * d'oh * make sure timer stops before callback (cf. luv example) * factor out timer, more documentation * fixup * validate function argument for schedule * fix block selection past eol * correct handling of multibyte characters * move arguments around, some cleanup * move utility functions to vim.lua * use anonymous namespaces, avoid local api * rename function * add test for schedule_fn * fix indent * turn hl-yank into proper (hightlight) module * factor out position-to-region function mark extraction now part of highlight.on_yank * rename schedule_fn to defer_fn * add test for vim.region * todo: handle double-width characters * remove debug printout * do not shadow arguments * defer also callable table * whitespace change * move highlight to vim/highlight.lua * add documentation * add @return documentation * test: add check before vim.defer fires * doc: fixup --- runtime/lua/vim/highlight.lua | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 runtime/lua/vim/highlight.lua (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/highlight.lua b/runtime/lua/vim/highlight.lua new file mode 100644 index 0000000000..5c98c626a4 --- /dev/null +++ b/runtime/lua/vim/highlight.lua @@ -0,0 +1,41 @@ +local api = vim.api + +local highlight = {} + +--- Highlight the yanked region +-- +--- use from init.vim via +--- au TextYankPost * lua require'vim.highlight'.on_yank() +--- customize highlight group and timeout via +--- au TextYankPost * lua require'vim.highlight'.on_yank("IncSearch", 500) +-- @param higroup highlight group for yanked region +-- @param timeout time in ms before highlight is cleared +-- @param event event structure +function highlight.on_yank(higroup, timeout, event) + event = event or vim.v.event + if event.operator ~= 'y' or event.regtype == '' then return end + higroup = higroup or "IncSearch" + timeout = timeout or 500 + + local bufnr = api.nvim_get_current_buf() + local yank_ns = api.nvim_create_namespace('') + api.nvim_buf_clear_namespace(bufnr, yank_ns, 0, -1) + + local pos1 = vim.fn.getpos("'[") + local pos2 = vim.fn.getpos("']") + + pos1 = {pos1[2] - 1, pos1[3] - 1 + pos1[4]} + pos2 = {pos2[2] - 1, pos2[3] - 1 + pos2[4]} + + local region = vim.region(bufnr, pos1, pos2, event.regtype, event.inclusive) + for linenr, cols in pairs(region) do + api.nvim_buf_add_highlight(bufnr, yank_ns, higroup, linenr, cols[1], cols[2]) + end + + vim.defer_fn( + function() api.nvim_buf_clear_namespace(bufnr, yank_ns, 0, -1) end, + timeout + ) +end + +return highlight -- cgit From 5126546ed77ca87eb7a5fd402ad0acaeae1d110d Mon Sep 17 00:00:00 2001 From: Mike Hartington Date: Mon, 18 May 2020 21:06:30 -0400 Subject: lsp: fix get diagnositcs --- runtime/lua/vim/lsp/util.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 534c90f1fd..5c6d183ac1 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -757,7 +757,7 @@ do local lines = {"Diagnostics:"} local highlights = {{0, "Bold"}} local line_diagnostics = M.get_line_diagnostics() - if not line_diagnostics then return end + if vim.tbl_isempty(line_diagnostics) then return end for i, diagnostic in ipairs(line_diagnostics) do -- for i, mark in ipairs(marks) do -- cgit From 131063e08fef32ee34732ba1530676bdf408d924 Mon Sep 17 00:00:00 2001 From: Andreas Johansson Date: Tue, 19 May 2020 08:49:13 +0200 Subject: Refactor fetching the line byte Takes the entire LSP position instead of line/col --- runtime/lua/vim/lsp/util.lua | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 534c90f1fd..8a536f72f4 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -94,17 +94,23 @@ local edit_sort_key = sort_by_key(function(e) return {e.A[1], e.A[2], e.i} end) -local function get_line_byte_from_line_character(bufnr, lnum, cnum) - -- Skip check when the byte and character position is the same - if cnum > 0 then - local lines = api.nvim_buf_get_lines(bufnr, lnum, lnum+1, false) - +--- Position is a https://microsoft.github.io/language-server-protocol/specifications/specification-current/#position +-- Returns a zero-indexed column, since set_lines() does the conversion to +-- 1-indexed +local function get_line_byte_from_position(bufnr, position) + -- LSP's line and characters are 0-indexed + -- Vim's line and columns are 1-indexed + local col = position.character + -- When on the first character, we can ignore the difference between byte and + -- character + if col > 0 then + local line = position.line + local lines = api.nvim_buf_get_lines(bufnr, line, line + 1, false) if #lines > 0 then - return vim.str_byteindex(lines[1], cnum) + return vim.str_byteindex(lines[1], col) end end - - return cnum + return col end function M.apply_text_edits(text_edits, bufnr) @@ -117,15 +123,9 @@ function M.apply_text_edits(text_edits, bufnr) for i, e in ipairs(text_edits) do -- adjust start and end column for UTF-16 encoding of non-ASCII characters local start_row = e.range.start.line - local start_col = get_line_byte_from_line_character( - bufnr, - start_row, - e.range.start.character) + local start_col = get_line_byte_from_position(bufnr, e.range.start) local end_row = e.range["end"].line - local end_col = get_line_byte_from_line_character( - bufnr, - end_row, - e.range["end"].character) + local end_col = get_line_byte_from_position(bufnr, e.range['end']) start_line = math.min(e.range.start.line, start_line) finish_line = math.max(e.range["end"].line, finish_line) -- TODO(ashkan) sanity check ranges for overlap. -- cgit From 0aca34e0a98ab47a8ae907031b25f687760a9264 Mon Sep 17 00:00:00 2001 From: Andreas Johansson Date: Tue, 19 May 2020 08:50:31 +0200 Subject: Use get_line_byte_from_position in jump_to_location --- runtime/lua/vim/lsp/util.lua | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 8a536f72f4..1b099fb3f4 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -474,9 +474,7 @@ function M.jump_to_location(location) api.nvim_buf_set_option(0, 'buflisted', true) local range = location.range or location.targetSelectionRange local row = range.start.line - local col = range.start.character - local line = api.nvim_buf_get_lines(0, row, row+1, true)[1] - col = vim.str_byteindex(line, col) + local col = get_line_byte_from_position(0, range.start) api.nvim_win_set_cursor(0, {row + 1, col}) return true end -- cgit From 044eb56ed2f44b545e7488990ecf195a930174aa Mon Sep 17 00:00:00 2001 From: Mathias Fußenegger Date: Thu, 21 May 2020 03:18:35 +0200 Subject: LSP: Don't swallow bufnr argument from callbacks (#12350) The callbacks for `textDocument/documentSymbol` and `workspace/symbol` never received the `bufnr` argument because the logic that adds error validation and logging swallowed the argument. --- runtime/lua/vim/lsp/callbacks.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/callbacks.lua b/runtime/lua/vim/lsp/callbacks.lua index 37e9f1e5c1..7c51fc2cc2 100644 --- a/runtime/lua/vim/lsp/callbacks.lua +++ b/runtime/lua/vim/lsp/callbacks.lua @@ -242,12 +242,12 @@ end -- Add boilerplate error validation and logging for all of these. for k, fn in pairs(M) do - M[k] = function(err, method, params, client_id) - local _ = log.debug() and log.debug('default_callback', method, { params = params, client_id = client_id, err = err }) + M[k] = function(err, method, params, client_id, bufnr) + log.debug('default_callback', method, { params = params, client_id = client_id, err = err, bufnr = bufnr }) if err then error(tostring(err)) end - return fn(err, method, params, client_id) + return fn(err, method, params, client_id, bufnr) end end -- cgit From 04a0486c66e2ae6d67cad990f95283863dbe28fd Mon Sep 17 00:00:00 2001 From: Mathias Fußenegger Date: Thu, 21 May 2020 18:17:21 +0200 Subject: Change uri_to_fname to not convert non-file URIs (#12351) * Change uri_to_fname to not convert non-file URIs A URI with a scheme other than file doesn't have a local file path. * fixup! Change uri_to_fname to not convert non-file URIs * fixup! fixup! Change uri_to_fname to not convert non-file URIs --- runtime/lua/vim/uri.lua | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/uri.lua b/runtime/lua/vim/uri.lua index e28cc9e20f..9c3535c676 100644 --- a/runtime/lua/vim/uri.lua +++ b/runtime/lua/vim/uri.lua @@ -65,9 +65,11 @@ local function uri_from_fname(path) return table.concat(uri_parts) end +local URI_SCHEME_PATTERN = '^([a-zA-Z]+[a-zA-Z0-9+-.]*)://.*' + local function uri_from_bufnr(bufnr) local fname = vim.api.nvim_buf_get_name(bufnr) - local scheme = fname:match("^([a-z]+)://.*") + local scheme = fname:match(URI_SCHEME_PATTERN) if scheme then return fname else @@ -76,6 +78,10 @@ local function uri_from_bufnr(bufnr) end local function uri_to_fname(uri) + local scheme = assert(uri:match(URI_SCHEME_PATTERN), 'URI must contain a scheme: ' .. uri) + if scheme ~= 'file' then + return uri + end uri = uri_decode(uri) -- TODO improve this. if is_windows_file_uri(uri) then @@ -89,7 +95,7 @@ end -- Return or create a buffer for a uri. local function uri_to_bufnr(uri) - local scheme = assert(uri:match("^([a-z]+)://.*"), 'Uri must contain a scheme: ' .. uri) + local scheme = assert(uri:match(URI_SCHEME_PATTERN), 'URI must contain a scheme: ' .. uri) if scheme == 'file' then return vim.fn.bufadd(uri_to_fname(uri)) else -- cgit From e0e5ada2bfa4c179062257857829bc27dbdeb580 Mon Sep 17 00:00:00 2001 From: Hirokazu Hata Date: Sat, 23 May 2020 15:01:08 +0900 Subject: lsp: change log name to "lsp.log" from "vim-lsp.log" It's confusing because vim-lsp already has the same name as the plugin name that predates this built-in lsp. Also, since "vim.fn.stdpath" is used, adding the prefix "nvim-" is redundant, so just "lsp.log" will suffice. --- runtime/lua/vim/lsp/log.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/log.lua b/runtime/lua/vim/lsp/log.lua index 78aabf08ce..696ce43a59 100644 --- a/runtime/lua/vim/lsp/log.lua +++ b/runtime/lua/vim/lsp/log.lua @@ -24,7 +24,7 @@ do local function path_join(...) return table.concat(vim.tbl_flatten{...}, path_sep) end - local logfilename = path_join(vim.fn.stdpath('data'), 'vim-lsp.log') + local logfilename = path_join(vim.fn.stdpath('data'), 'lsp.log') --- Return the log filename. function log.get_filename() -- cgit From 15b762761ad966f91d452fdd28c718f2fd3e45be Mon Sep 17 00:00:00 2001 From: Hirokazu Hata Date: Tue, 26 May 2020 21:55:45 +0900 Subject: lsp: make the command error message more detailed (#11633) * lsp.lua: make the error message more detailed * test: add lsp._cmd_part test --- runtime/lua/vim/lsp.lua | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 61da2130c8..7135d2c5b6 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -122,19 +122,19 @@ local function validate_encoding(encoding) end function lsp._cmd_parts(input) - local cmd, cmd_args - if vim.tbl_islist(input) then - cmd = input[1] - cmd_args = {} - -- Don't mutate our input. - for i, v in ipairs(input) do - assert(type(v) == 'string', "input arguments must be strings") - if i > 1 then - table.insert(cmd_args, v) - end + vim.validate{cmd={ + input, + function() return vim.tbl_islist(input) end, + "list"}} + + local cmd = input[1] + local cmd_args = {} + -- Don't mutate our input. + for i, v in ipairs(input) do + vim.validate{["cmd argument"]={v, "s"}} + if i > 1 then + table.insert(cmd_args, v) end - else - error("cmd type must be list.") end return cmd, cmd_args end @@ -524,7 +524,7 @@ function lsp.start_client(config) function client.request(method, params, callback, bufnr) if not callback then callback = resolve_callback(method) - or error("not found: request callback for client "..client.name) + or error(string.format("not found: %q request callback for client %q.", method, client.name)) end local _ = log.debug() and log.debug(log_prefix, "client.request", client_id, method, params, callback, bufnr) -- TODO keep these checks or just let it go anyway? -- cgit From 2ca8f02a6461fd4710c4ecc555fbe7ee9f75a70a Mon Sep 17 00:00:00 2001 From: Christian Clason Date: Tue, 26 May 2020 15:07:10 +0200 Subject: lsp: add preview_location util function (#12368) * add preview_location * add doc stub * doc style; return bufnr&winnr of preview * doc: function may return nil Co-authored-by: Hirokazu Hata * doc: fixup Co-authored-by: Hirokazu Hata --- runtime/lua/vim/lsp/util.lua | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 5c6d183ac1..b6eaae1fef 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -481,6 +481,28 @@ function M.jump_to_location(location) return true end +--- Preview a location in a floating windows +--- +--- behavior depends on type of location: +--- - for Location, range is shown (e.g., function definition) +--- - for LocationLink, targetRange is shown (e.g., body of function definition) +--- +--@param location a single Location or LocationLink +--@return bufnr,winnr buffer and window number of floating window or nil +function M.preview_location(location) + -- location may be LocationLink or Location (more useful for the former) + local uri = location.targetUri or location.uri + if uri == nil then return end + local bufnr = vim.uri_to_bufnr(uri) + if not api.nvim_buf_is_loaded(bufnr) then + vim.fn.bufload(bufnr) + end + local range = location.targetRange or location.range + local contents = api.nvim_buf_get_lines(bufnr, range.start.line, range["end"].line+1, false) + local filetype = api.nvim_buf_get_option(bufnr, 'filetype') + return M.open_floating_preview(contents, filetype) +end + local function find_window_by_var(name, value) for _, win in ipairs(api.nvim_list_wins()) do if npcall(api.nvim_win_get_var, win, name) == value then -- cgit From 5a9226c800d3075821203952da7c38626180680d Mon Sep 17 00:00:00 2001 From: Viktor Kojouharov Date: Thu, 28 May 2020 14:31:56 +0200 Subject: lua: simple snippet support in the completion items (#12118) Old behavior is: foo(${placeholder: bar, ...) with lots of random garbage you'd never want inserted. New behavior is: foo(bar, baz) (which maybe is good, maybe is bad [depends on user], but definitely better than it was). ----- * Implement rudimentary snippet parsing Add support for parsing and discarding snippet tokens from the completion items. Fixes #11982 * Enable snippet support * Functional tests for snippet parsing Add simplified real-world snippet text examples to the completion items test * Add a test for nested snippet tokens * Remove TODO comment * Return the unmodified item if the format is plain text * Add a plain text completion item --- runtime/lua/vim/lsp/protocol.lua | 3 +- runtime/lua/vim/lsp/util.lua | 72 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 71 insertions(+), 4 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index 877d11411b..7d5f8f5ef1 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -633,8 +633,7 @@ function protocol.make_client_capabilities() dynamicRegistration = false; completionItem = { - -- TODO(tjdevries): Is it possible to implement this in plain lua? - snippetSupport = false; + snippetSupport = true; commitCharactersSupport = false; preselectSupport = false; deprecatedSupport = false; diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index b6eaae1fef..752d4ff439 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -199,6 +199,66 @@ function M.get_current_line_to_cursor() return line:sub(pos[2]+1) end +local function parse_snippet_rec(input, inner) + local res = "" + + local close, closeend = nil, nil + if inner then + close, closeend = input:find("}", 1, true) + while close ~= nil and input:sub(close-1,close-1) == "\\" do + close, closeend = input:find("}", closeend+1, true) + end + end + + local didx = input:find('$', 1, true) + if didx == nil and close == nil then + return input, "" + elseif close ~=nil and (didx == nil or close < didx) then + -- No inner placeholders + return input:sub(0, close-1), input:sub(closeend+1) + end + + res = res .. input:sub(0, didx-1) + input = input:sub(didx+1) + + local tabstop, tabstopend = input:find('^%d+') + local placeholder, placeholderend = input:find('^{%d+:') + local choice, choiceend = input:find('^{%d+|') + + if tabstop then + input = input:sub(tabstopend+1) + elseif choice then + input = input:sub(choiceend+1) + close, closeend = input:find("|}", 1, true) + + res = res .. input:sub(0, close-1) + input = input:sub(closeend+1) + elseif placeholder then + -- TODO: add support for variables + input = input:sub(placeholderend+1) + + -- placeholders and variables are recursive + while input ~= "" do + local r, tail = parse_snippet_rec(input, true) + r = r:gsub("\\}", "}") + + res = res .. r + input = tail + end + else + res = res .. "$" + end + + return res, input +end + +-- Parse completion entries, consuming snippet tokens +function M.parse_snippet(input) + local res, _ = parse_snippet_rec(input, false) + + return res +end + -- Sort by CompletionItem.sortText -- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion local function sort_completion_items(items) @@ -213,9 +273,17 @@ end -- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion local function get_completion_word(item) if item.textEdit ~= nil and item.textEdit.newText ~= nil then - return item.textEdit.newText + if protocol.InsertTextFormat[item.insertTextFormat] == "PlainText" then + return item.textEdit.newText + else + return M.parse_snippet(item.textEdit.newText) + end elseif item.insertText ~= nil then - return item.insertText + if protocol.InsertTextFormat[item.insertTextFormat] == "PlainText" then + return item.insertText + else + return M.parse_snippet(item.insertText) + end end return item.label end -- cgit From be662fe5c7d06ee63377dd7defb72aea88134305 Mon Sep 17 00:00:00 2001 From: TJ DeVries Date: Wed, 20 May 2020 11:08:19 -0400 Subject: lua: vim.wait implementation --- runtime/lua/vim/lsp.lua | 37 +++++++------------------------------ 1 file changed, 7 insertions(+), 30 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 7135d2c5b6..84812b8c64 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -46,31 +46,6 @@ local function is_dir(filename) return stat and stat.type == 'directory' or false end --- TODO Use vim.wait when that is available, but provide an alternative for now. -local wait = vim.wait or function(timeout_ms, condition, interval) - validate { - timeout_ms = { timeout_ms, 'n' }; - condition = { condition, 'f' }; - interval = { interval, 'n', true }; - } - assert(timeout_ms > 0, "timeout_ms must be > 0") - local _ = log.debug() and log.debug("wait.fallback", timeout_ms) - interval = interval or 200 - local interval_cmd = "sleep "..interval.."m" - local timeout = timeout_ms + uv.now() - -- TODO is there a better way to sync this? - while true do - uv.update_time() - if condition() then - return 0 - end - if uv.now() >= timeout then - return -1 - end - nvim_command(interval_cmd) - -- vim.loop.sleep(10) - end -end local wait_result_reason = { [-1] = "timeout"; [-2] = "interrupted"; [-3] = "error" } local valid_encodings = { @@ -810,8 +785,8 @@ function lsp._vim_exit_handler() for _, client in pairs(active_clients) do client.stop() end - local wait_result = wait(500, function() return tbl_isempty(active_clients) end, 50) - if wait_result ~= 0 then + + if not vim.wait(500, function() return tbl_isempty(active_clients) end, 50) then for _, client in pairs(active_clients) do client.stop(true) end @@ -889,12 +864,14 @@ function lsp.buf_request_sync(bufnr, method, params, timeout_ms) for _ in pairs(client_request_ids) do expected_result_count = expected_result_count + 1 end - local wait_result = wait(timeout_ms or 100, function() + + local wait_result, reason = vim.wait(timeout_ms or 100, function() return result_count >= expected_result_count end, 10) - if wait_result ~= 0 then + + if not wait_result then cancel() - return nil, wait_result_reason[wait_result] + return nil, wait_result_reason[reason] end return request_results end -- cgit From 91e41c857622b61adcf84f6a6af87a3f5a4d65f5 Mon Sep 17 00:00:00 2001 From: Christian Clason Date: Sun, 31 May 2020 20:56:00 +0200 Subject: lua: add vim.highlight.range (#12401) --- runtime/lua/vim/highlight.lua | 29 ++++++++++++++++++++++++----- runtime/lua/vim/lsp/util.lua | 25 ++++++------------------- 2 files changed, 30 insertions(+), 24 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/highlight.lua b/runtime/lua/vim/highlight.lua index 5c98c626a4..69c3c8a4dc 100644 --- a/runtime/lua/vim/highlight.lua +++ b/runtime/lua/vim/highlight.lua @@ -2,12 +2,34 @@ local api = vim.api local highlight = {} +--- Highlight range between two positions +--- +--@param bufnr number of buffer to apply highlighting to +--@param ns namespace to add highlight to +--@param higroup highlight group to use for highlighting +--@param rtype type of range (:help setreg, default charwise) +--@param inclusive boolean indicating whether the range is end-inclusive (default false) +function highlight.range(bufnr, ns, higroup, start, finish, rtype, inclusive) + rtype = rtype or 'v' + inclusive = inclusive or false + + -- sanity check + if start[2] < 0 or finish[2] < start[2] then return end + + local region = vim.region(bufnr, start, finish, rtype, inclusive) + for linenr, cols in pairs(region) do + api.nvim_buf_add_highlight(bufnr, ns, higroup, linenr, cols[1], cols[2]) + end + +end + --- Highlight the yanked region --- +--- --- use from init.vim via --- au TextYankPost * lua require'vim.highlight'.on_yank() --- customize highlight group and timeout via --- au TextYankPost * lua require'vim.highlight'.on_yank("IncSearch", 500) +--- -- @param higroup highlight group for yanked region -- @param timeout time in ms before highlight is cleared -- @param event event structure @@ -27,10 +49,7 @@ function highlight.on_yank(higroup, timeout, event) pos1 = {pos1[2] - 1, pos1[3] - 1 + pos1[4]} pos2 = {pos2[2] - 1, pos2[3] - 1 + pos2[4]} - local region = vim.region(bufnr, pos1, pos2, event.regtype, event.inclusive) - for linenr, cols in pairs(region) do - api.nvim_buf_add_highlight(bufnr, yank_ns, higroup, linenr, cols[1], cols[2]) - end + highlight.range(bufnr, yank_ns, higroup, pos1, pos2, event.regtype, event.inclusive) vim.defer_fn( function() api.nvim_buf_clear_namespace(bufnr, yank_ns, 0, -1) end, diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 752d4ff439..79d428d12d 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -3,6 +3,7 @@ local vim = vim local validate = vim.validate local api = vim.api local list_extend = vim.list_extend +local highlight = require 'vim.highlight' local M = {} @@ -691,7 +692,7 @@ function M.fancy_floating_markdown(contents, opts) vim.cmd("ownsyntax markdown") local idx = 1 - local function highlight_region(ft, start, finish) + local function apply_syntax_to_region(ft, start, finish) if ft == '' then return end local name = ft..idx idx = idx + 1 @@ -707,8 +708,8 @@ function M.fancy_floating_markdown(contents, opts) -- make sure that regions between code blocks are definitely markdown. -- local ph = {start = 0; finish = 1;} for _, h in ipairs(highlights) do - -- highlight_region('markdown', ph.finish, h.start) - highlight_region(h.ft, h.start, h.finish) + -- apply_syntax_to_region('markdown', ph.finish, h.start) + apply_syntax_to_region(h.ft, h.start, h.finish) -- ph = h end @@ -762,19 +763,6 @@ function M.open_floating_preview(contents, filetype, opts) return floating_bufnr, floating_winnr end -local function highlight_range(bufnr, ns, hiname, start, finish) - if start[1] == finish[1] then - -- TODO care about encoding here since this is in byte index? - api.nvim_buf_add_highlight(bufnr, ns, hiname, start[1], start[2], finish[2]) - else - api.nvim_buf_add_highlight(bufnr, ns, hiname, start[1], start[2], -1) - for line = start[1] + 1, finish[1] - 1 do - api.nvim_buf_add_highlight(bufnr, ns, hiname, line, 0, -1) - end - api.nvim_buf_add_highlight(bufnr, ns, hiname, finish[1], 0, finish[2]) - end -end - do local diagnostic_ns = api.nvim_create_namespace("vim_lsp_diagnostics") local reference_ns = api.nvim_create_namespace("vim_lsp_references") @@ -908,8 +896,7 @@ do [protocol.DiagnosticSeverity.Hint]='Hint', } - -- TODO care about encoding here since this is in byte index? - highlight_range(bufnr, diagnostic_ns, + highlight.range(bufnr, diagnostic_ns, underline_highlight_name..hlmap[diagnostic.severity], {start.line, start.character}, {finish.line, finish.character} @@ -933,7 +920,7 @@ do [protocol.DocumentHighlightKind.Write] = "LspReferenceWrite"; } local kind = reference["kind"] or protocol.DocumentHighlightKind.Text - highlight_range(bufnr, reference_ns, document_highlight_kind[kind], start_pos, end_pos) + highlight.range(bufnr, reference_ns, document_highlight_kind[kind], start_pos, end_pos) end end -- cgit From 60c581b35db439dd6b32cdc2ebe1a5aed933b44c Mon Sep 17 00:00:00 2001 From: notomo Date: Wed, 3 Jun 2020 08:31:43 +0900 Subject: lua: fix infinite loop for vim.split on empty string (#12420) --- runtime/lua/vim/shared.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 2135bfc837..5dc8d6dc10 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -79,7 +79,7 @@ function vim.gsplit(s, sep, plain) end return function() - if done then + if done or s == '' then return end if sep == '' then -- cgit From 6a93077475d298f46ac19c8030ed5b4a723685dc Mon Sep 17 00:00:00 2001 From: Thomas Vigouroux Date: Wed, 3 Jun 2020 19:58:02 +0200 Subject: treesitter: fix tests --- runtime/lua/vim/treesitter.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua index d3b78a7f73..c502e45bd0 100644 --- a/runtime/lua/vim/treesitter.lua +++ b/runtime/lua/vim/treesitter.lua @@ -162,16 +162,17 @@ function Query:match_preds(match, pattern, bufnr) -- Here we only want to return if a predicate DOES NOT match, and -- continue on the other case. This way unknown predicates will not be considered, -- which allows some testing and easier user extensibility (#12173). + -- Also, tree-sitter strips the leading # from predicates for us. if pred[1] == "eq?" then local node = match[pred[2]] local node_text = get_node_text(node, bufnr) local str if type(pred[3]) == "string" then - -- (eq? @aa "foo") + -- (#eq? @aa "foo") str = pred[3] else - -- (eq? @aa @bb) + -- (#eq? @aa @bb) str = get_node_text(match[pred[3]], bufnr) end -- cgit From ac5a3f2c56775040a1b7de438cfd592c0dc26400 Mon Sep 17 00:00:00 2001 From: notomo Date: Thu, 4 Jun 2020 21:48:48 +0900 Subject: lua: fix behavior when split empty string (#12429) * lua: fix behavior when split empty string * test: lsp.util.apply_text_edits with an empty edit --- runtime/lua/vim/shared.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 5dc8d6dc10..384d22cb89 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -79,7 +79,7 @@ function vim.gsplit(s, sep, plain) end return function() - if done or s == '' then + if done or (s == '' and sep == '') then return end if sep == '' then -- cgit From 6f4f38cd54693ca99c887756e6179cc0775377d0 Mon Sep 17 00:00:00 2001 From: Dheepak Krishnamurthy Date: Thu, 4 Jun 2020 06:52:44 -0600 Subject: lsp: Add check for `declaration` and `typeDefinition` support in vim lsp server before making `request` (#12421) * Add check for typeDefinition support in vim lsp server * Check for typeDefinitionProvider in server * Check for declarationProvider in server * Add check for client support * Fix typo --- runtime/lua/vim/lsp.lua | 2 ++ runtime/lua/vim/lsp/protocol.lua | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 84812b8c64..2fbc51481f 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -507,6 +507,8 @@ function lsp.start_client(config) or (not client.resolved_capabilities.signature_help and method == 'textDocument/signatureHelp') or (not client.resolved_capabilities.goto_definition and method == 'textDocument/definition') or (not client.resolved_capabilities.implementation and method == 'textDocument/implementation') + or (not client.resolved_capabilities.declaration and method == 'textDocument/declaration') + or (not client.resolved_capabilities.type_definition and method == 'textDocument/typeDefinition') or (not client.resolved_capabilities.document_symbol and method == 'textDocument/documentSymbol') or (not client.resolved_capabilities.workspace_symbol and method == 'textDocument/workspaceSymbol') then diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index 7d5f8f5ef1..64911fe7bb 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -923,6 +923,28 @@ function protocol.resolve_capabilities(server_capabilities) error("The server sent invalid codeActionProvider") end + if server_capabilities.declarationProvider == nil then + general_properties.declaration = false + elseif type(server_capabilities.declarationProvider) == 'boolean' then + general_properties.declaration = server_capabilities.declarationProvider + elseif type(server_capabilities.declarationProvider) == 'table' then + -- TODO: support more detailed declarationProvider options. + general_properties.declaration = false + else + error("The server sent invalid declarationProvider") + end + + if server_capabilities.typeDefinitionProvider == nil then + general_properties.type_definition = false + elseif type(server_capabilities.typeDefinitionProvider) == 'boolean' then + general_properties.type_definition = server_capabilities.typeDefinitionProvider + elseif type(server_capabilities.typeDefinitionProvider) == 'table' then + -- TODO: support more detailed typeDefinitionProvider options. + general_properties.type_definition = false + else + error("The server sent invalid typeDefinitionProvider") + end + if server_capabilities.implementationProvider == nil then general_properties.implementation = false elseif type(server_capabilities.implementationProvider) == 'boolean' then -- cgit From b7f3f11049c6847a2b0c4bbd89e8339036e00da6 Mon Sep 17 00:00:00 2001 From: Christian Clason Date: Thu, 4 Jun 2020 20:23:03 +0200 Subject: lsp: compute height of floating preview correctly for wrapped lines (#12380) * take wrapping into account when computing float height * factor out size calculation * add test * accept and pass through opts.wrap_at in floating_preview * make padding configurable * slightly refactor fancy_floating_markdown to make use of make_position * padding using string.format * move trim and pad to separate function * nit Co-authored-by: Hirokazu Hata * remove mention of backward compat * make lint happy Co-authored-by: Hirokazu Hata --- runtime/lua/vim/lsp/util.lua | 152 +++++++++++++++++++++++++++++++++---------- 1 file changed, 118 insertions(+), 34 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 02d233fb7b..49e2557c16 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -289,7 +289,7 @@ local function get_completion_word(item) return item.label end --- Some lanuguage servers return complementary candidates whose prefixes do not match are also returned. +-- Some language servers return complementary candidates whose prefixes do not match are also returned. -- So we exclude completion candidates whose prefix does not match. local function remove_unmatch_completion_items(items, prefix) return vim.tbl_filter(function(item) @@ -614,13 +614,53 @@ function M.focusable_preview(unique_name, fn) end) end --- Convert markdown into syntax highlighted regions by stripping the code --- blocks and converting them into highlighted code. --- This will by default insert a blank line separator after those code block --- regions to improve readability. +--- Trim empty lines from input and pad left and right with spaces +--- +--@param contents table of lines to trim and pad +--@param opts dictionary with optional fields +-- - pad_left amount of columns to pad contents at left (default 1) +-- - pad_right amount of columns to pad contents at right (default 1) +--@return contents table of trimmed and padded lines +function M._trim_and_pad(contents, opts) + validate { + contents = { contents, 't' }; + opts = { opts, 't', true }; + } + opts = opts or {} + local left_padding = (" "):rep(opts.pad_left or 1) + local right_padding = (" "):rep(opts.pad_right or 1) + contents = M.trim_empty_lines(contents) + for i, line in ipairs(contents) do + contents[i] = string.format('%s%s%s', left_padding, line:gsub("\r", ""), right_padding) + end + return contents +end + + + +--- Convert markdown into syntax highlighted regions by stripping the code +--- blocks and converting them into highlighted code. +--- This will by default insert a blank line separator after those code block +--- regions to improve readability. +--- The result is shown in a floating preview +--- TODO: refactor to separate stripping/converting and make use of open_floating_preview +--- +--@param contents table of lines to show in window +--@param opts dictionary with optional fields +-- - height of floating window +-- - width of floating window +-- - wrap_at character to wrap at for computing height +-- - pad_left amount of columns to pad contents at left +-- - pad_right amount of columns to pad contents at right +-- - separator insert separator after code block +--@return width,height size of float function M.fancy_floating_markdown(contents, opts) - local pad_left = opts and opts.pad_left - local pad_right = opts and opts.pad_right + validate { + contents = { contents, 't' }; + opts = { opts, 't', true }; + } + opts = opts or {} + local stripped = {} local highlights = {} do @@ -654,31 +694,27 @@ function M.fancy_floating_markdown(contents, opts) end end end - local width = 0 - for i, v in ipairs(stripped) do - v = v:gsub("\r", "") - if pad_left then v = (" "):rep(pad_left)..v end - if pad_right then v = v..(" "):rep(pad_right) end - stripped[i] = v - width = math.max(width, #v) - end - if opts and opts.max_width then - width = math.min(opts.max_width, width) - end - -- TODO(ashkan): decide how to make this customizable. - local insert_separator = true + -- Clean up and add padding + stripped = M._trim_and_pad(stripped, opts) + + -- Compute size of float needed to show (wrapped) lines + opts.wrap_at = opts.wrap_at or (vim.wo["wrap"] and api.nvim_win_get_width(0)) + local width, height = M._make_floating_popup_size(stripped, opts) + + -- Insert blank line separator after code block + local insert_separator = opts.separator or true if insert_separator then for i, h in ipairs(highlights) do h.start = h.start + i - 1 h.finish = h.finish + i - 1 if h.finish + 1 <= #stripped then table.insert(stripped, h.finish + 1, string.rep("─", width)) + height = height + 1 end end end -- Make the floating window. - local height = #stripped local bufnr = api.nvim_create_buf(false, true) local winnr = api.nvim_open_win(bufnr, false, M.make_floating_popup_options(width, height, opts)) vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, stripped) @@ -719,33 +755,81 @@ function M.close_preview_autocmd(events, winnr) api.nvim_command("autocmd "..table.concat(events, ',').." ++once lua pcall(vim.api.nvim_win_close, "..winnr..", true)") end -function M.open_floating_preview(contents, filetype, opts) +--- Compute size of float needed to show contents (with optional wrapping) +--- +--@param contents table of lines to show in window +--@param opts dictionary with optional fields +-- - height of floating window +-- - width of floating window +-- - wrap_at character to wrap at for computing height +--@return width,height size of float +function M._make_floating_popup_size(contents, opts) validate { contents = { contents, 't' }; - filetype = { filetype, 's', true }; opts = { opts, 't', true }; } opts = opts or {} - -- Trim empty lines from the end. - contents = M.trim_empty_lines(contents) - local width = opts.width - local height = opts.height or #contents + local height = opts.height + local line_widths = {} + if not width then width = 0 for i, line in ipairs(contents) do - -- Clean up the input and add left pad. - line = " "..line:gsub("\r", "") -- TODO(ashkan) use nvim_strdisplaywidth if/when that is introduced. - local line_width = vim.fn.strdisplaywidth(line) - width = math.max(line_width, width) - contents[i] = line + line_widths[i] = vim.fn.strdisplaywidth(line) + width = math.max(line_widths[i], width) end - -- Add right padding of 1 each. - width = width + 1 end + if not height then + height = #contents + local wrap_at = opts.wrap_at + if wrap_at and width > wrap_at then + height = 0 + if vim.tbl_isempty(line_widths) then + for _, line in ipairs(contents) do + local line_width = vim.fn.strdisplaywidth(line) + height = height + math.ceil(line_width/wrap_at) + end + else + for i = 1, #contents do + height = height + math.ceil(line_widths[i]/wrap_at) + end + end + end + end + + return width, height +end + +--- Show contents in a floating window +--- +--@param contents table of lines to show in window +--@param filetype string of filetype to set for opened buffer +--@param opts dictionary with optional fields +-- - height of floating window +-- - width of floating window +-- - wrap_at character to wrap at for computing height +-- - pad_left amount of columns to pad contents at left +-- - pad_right amount of columns to pad contents at right +--@return bufnr,winnr buffer and window number of floating window or nil +function M.open_floating_preview(contents, filetype, opts) + validate { + contents = { contents, 't' }; + filetype = { filetype, 's', true }; + opts = { opts, 't', true }; + } + opts = opts or {} + + -- Clean up input: trim empty lines from the end, pad + contents = M._trim_and_pad(contents, opts) + + -- Compute size of float needed to show (wrapped) lines + opts.wrap_at = opts.wrap_at or (vim.wo["wrap"] and api.nvim_win_get_width(0)) + local width, height = M._make_floating_popup_size(contents, opts) + local floating_bufnr = api.nvim_create_buf(false, true) if filetype then api.nvim_buf_set_option(floating_bufnr, 'filetype', filetype) -- cgit From dd4018947c9f9b39b4e473c21ebf0e27f1e7ddc5 Mon Sep 17 00:00:00 2001 From: TJ DeVries Date: Thu, 4 Jun 2020 19:37:38 -0400 Subject: lsp: do not process diagnostics for unloaded buffers (#12440) --- runtime/lua/vim/lsp/callbacks.lua | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/callbacks.lua b/runtime/lua/vim/lsp/callbacks.lua index 7c51fc2cc2..4b14f0132d 100644 --- a/runtime/lua/vim/lsp/callbacks.lua +++ b/runtime/lua/vim/lsp/callbacks.lua @@ -72,6 +72,17 @@ M['textDocument/publishDiagnostics'] = function(_, _, result) err_message("LSP.publishDiagnostics: Couldn't find buffer for ", uri) return end + + -- Unloaded buffers should not handle diagnostics. + -- When the buffer is loaded, we'll call on_attach, which sends textDocument/didOpen. + -- This should trigger another publish of the diagnostics. + -- + -- In particular, this stops a ton of spam when first starting a server for current + -- unloaded buffers. + if not api.nvim_buf_is_loaded(bufnr) then + return + end + util.buf_clear_diagnostics(bufnr) -- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#diagnostic -- cgit From e39ec50d73cc5420a567a1678ff254341e17dca8 Mon Sep 17 00:00:00 2001 From: Hirokazu Hata Date: Thu, 11 Jun 2020 13:09:05 +0900 Subject: lsp: even if contents before change is 0 byte, request to server fix: https://github.com/neovim/neovim/issues/12414 --- runtime/lua/vim/lsp.lua | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 2fbc51481f..e759511347 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -586,9 +586,7 @@ do old_utf16_size) local _ = log.debug() and log.debug("on_lines", bufnr, changedtick, firstline, lastline, new_lastline, old_byte_size, old_utf32_size, old_utf16_size, nvim_buf_get_lines(bufnr, firstline, new_lastline, true)) - if old_byte_size == 0 then - return - end + -- Don't do anything if there are no clients attached. if tbl_isempty(all_buffer_active_clients[bufnr] or {}) then return -- cgit From b751d16cadfc3d2c7e7958432956859b3e2482fa Mon Sep 17 00:00:00 2001 From: Stephan Seitz Date: Thu, 11 Jun 2020 21:47:03 +0200 Subject: lsp: Fix #12449 textDocumentSync.save can be boolean. Access textDocumentSync.save.includeText only if table. (#12450) --- runtime/lua/vim/lsp/protocol.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index 64911fe7bb..4fded1961d 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -897,7 +897,8 @@ function protocol.resolve_capabilities(server_capabilities) text_document_will_save = ifnil(textDocumentSync.willSave, false); text_document_will_save_wait_until = ifnil(textDocumentSync.willSaveWaitUntil, false); text_document_save = ifnil(textDocumentSync.save, false); - text_document_save_include_text = ifnil(textDocumentSync.save and textDocumentSync.save.includeText, false); + text_document_save_include_text = ifnil(type(textDocumentSync.save) == 'table' + and textDocumentSync.save.includeText, false); } else return nil, string.format("Invalid type for textDocumentSync: %q", type(textDocumentSync)) -- cgit From a0a84fc9e0ba8631db35cfd7aa6a458fbdd80417 Mon Sep 17 00:00:00 2001 From: Dheepak Krishnamurthy Date: Fri, 12 Jun 2020 12:38:33 -0600 Subject: lsp: Add `BufLeave` to `close_preview_autocmd` function call (#12477) else popup window remains open when switching buffer. --- runtime/lua/vim/lsp/util.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 49e2557c16..a4ceeb34e6 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -841,7 +841,7 @@ function M.open_floating_preview(contents, filetype, opts) end api.nvim_buf_set_lines(floating_bufnr, 0, -1, true, contents) api.nvim_buf_set_option(floating_bufnr, 'modifiable', false) - M.close_preview_autocmd({"CursorMoved", "CursorMovedI", "BufHidden"}, floating_winnr) + M.close_preview_autocmd({"CursorMoved", "CursorMovedI", "BufHidden", "BufLeave"}, floating_winnr) return floating_bufnr, floating_winnr end -- cgit From 44fe8828f06a22bc9aa3617a6fd8aae447a838de Mon Sep 17 00:00:00 2001 From: Andreas Johansson Date: Sun, 14 Jun 2020 21:23:16 +0200 Subject: lsp: Fix text edits with the same start position (#12434) According to the LSP spec[1], multiple edits can have the same starting position, and if that is the case, they should be applied in the order as they come in the array. The implementation uses a reverse sort to not interfere with non applied edits, but failed to take into account the spec. [1] https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#textedit --- runtime/lua/vim/lsp/util.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index a4ceeb34e6..4c3c4fa6cb 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -92,7 +92,7 @@ local function sort_by_key(fn) end end local edit_sort_key = sort_by_key(function(e) - return {e.A[1], e.A[2], e.i} + return {e.A[1], e.A[2], -e.i} end) --- Position is a https://microsoft.github.io/language-server-protocol/specifications/specification-current/#position -- cgit From 70d4b31b834395fe36f4886e43b63fd4dc62ded1 Mon Sep 17 00:00:00 2001 From: francisco souza <108725+fsouza@users.noreply.github.com> Date: Thu, 18 Jun 2020 08:04:49 -0400 Subject: lsp: Add new highlight groups used in show_line_diagnostics (#12473) * lsp: support custom hl groups in show_line_diagnostics Closes #12472. * runtime: add docs for the new lsp highlight groups Co-authored-by: francisco souza --- runtime/lua/vim/lsp/util.lua | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 4c3c4fa6cb..71ca1048e1 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -859,6 +859,8 @@ do local severity_highlights = {} + local severity_floating_highlights = {} + local default_severity_highlight = { [protocol.DiagnosticSeverity.Error] = { guifg = "Red" }; [protocol.DiagnosticSeverity.Warning] = { guifg = "Orange" }; @@ -870,6 +872,7 @@ do for severity, hi_info in pairs(default_severity_highlight) do local severity_name = protocol.DiagnosticSeverity[severity] local highlight_name = "LspDiagnostics"..severity_name + local floating_highlight_name = highlight_name.."Floating" -- Try to fill in the foreground color with a sane default. local cmd_parts = {"highlight", "default", highlight_name} for k, v in pairs(hi_info) do @@ -877,7 +880,9 @@ do end api.nvim_command(table.concat(cmd_parts, ' ')) api.nvim_command('highlight link ' .. highlight_name .. 'Sign ' .. highlight_name) + api.nvim_command('highlight link ' .. highlight_name .. 'Floating ' .. highlight_name) severity_highlights[severity] = highlight_name + severity_floating_highlights[severity] = floating_highlight_name end function M.buf_clear_diagnostics(bufnr) @@ -926,7 +931,7 @@ do -- TODO(ashkan) make format configurable? local prefix = string.format("%d. ", i) - local hiname = severity_highlights[diagnostic.severity] + local hiname = severity_floating_highlights[diagnostic.severity] assert(hiname, 'unknown severity: ' .. tostring(diagnostic.severity)) local message_lines = split_lines(diagnostic.message) table.insert(lines, prefix..message_lines[1]) -- cgit From ebee9ebe2be2bd584efefc4064a8b3a07e0505e0 Mon Sep 17 00:00:00 2001 From: David Lukes Date: Tue, 5 May 2020 17:23:45 +0200 Subject: lsp: Add sync variant of LSP formatting Also, factor out a `vim.lsp.util.get_effective_tabstop()` helper and add tests for it. --- runtime/lua/vim/lsp/buf.lua | 19 +++++++++---------- runtime/lua/vim/lsp/util.lua | 24 ++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 10 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 7a819f3c3d..81d78c8b98 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -65,19 +65,18 @@ function M.completion(context) end function M.formatting(options) - validate { options = {options, 't', true} } - local sts = vim.bo.softtabstop; - options = vim.tbl_extend('keep', options or {}, { - tabSize = (sts > 0 and sts) or (sts < 0 and vim.bo.shiftwidth) or vim.bo.tabstop; - insertSpaces = vim.bo.expandtab; - }) - local params = { - textDocument = { uri = vim.uri_from_bufnr(0) }; - options = options; - } + local params = util.make_formatting_params(options) return request('textDocument/formatting', params) end +function M.formatting_sync(options, timeout_ms) + local params = util.make_formatting_params(options) + local result = vim.lsp.buf_request_sync(0, "textDocument/formatting", params, timeout_ms) + if not result then return end + result = result[1].result + vim.lsp.util.apply_text_edits(result) +end + function M.range_formatting(options, start_pos, end_pos) validate { options = {options, 't', true}; diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 71ca1048e1..2b9b4d8c7e 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -1259,6 +1259,30 @@ function M.make_text_document_params() return { uri = vim.uri_from_bufnr(0) } end +--- Get visual width of tabstop. +--- +--@see |softtabstop| +--@param bufnr (optional, number): Buffer handle, defaults to current +--@returns (number) tabstop visual width +function M.get_effective_tabstop(bufnr) + validate { bufnr = {bufnr, 'n', true} } + local bo = bufnr and vim.bo[bufnr] or vim.bo + local sts = bo.softtabstop + return (sts > 0 and sts) or (sts < 0 and bo.shiftwidth) or bo.tabstop +end + +function M.make_formatting_params(options) + validate { options = {options, 't', true} } + options = vim.tbl_extend('keep', options or {}, { + tabSize = M.get_effective_tabstop(); + insertSpaces = vim.bo.expandtab; + }) + return { + textDocument = { uri = vim.uri_from_bufnr(0) }; + options = options; + } +end + -- @param buf buffer handle or 0 for current. -- @param row 0-indexed line -- @param col 0-indexed byte offset in line -- cgit From 2f6d1d3c887a87d9402137425b418dd12a904aac Mon Sep 17 00:00:00 2001 From: francisco souza <108725+fsouza@users.noreply.github.com> Date: Tue, 23 Jun 2020 11:50:37 -0400 Subject: lsp: when apply text edits, set buflisted on buffers (#12489) The client creates buffers on the fly to be able to apply text edits on files that weren't previously open, which is great, but it uses the bufadd() function, which creates unlisted buffers and can lead to a weird experience in the text editor. Setting the buffer to buflisted fixes this. Closes #12488. Co-authored-by: francisco souza --- runtime/lua/vim/lsp/util.lua | 1 + 1 file changed, 1 insertion(+) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 71ca1048e1..0f366c2b57 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -119,6 +119,7 @@ function M.apply_text_edits(text_edits, bufnr) if not api.nvim_buf_is_loaded(bufnr) then vim.fn.bufload(bufnr) end + api.nvim_buf_set_option(bufnr, 'buflisted', true) local start_line, finish_line = math.huge, -1 local cleaned = {} for i, e in ipairs(text_edits) do -- cgit From 48ac77a14c65c18f1f4cd4ac8e657c645e9ca101 Mon Sep 17 00:00:00 2001 From: cbarrete <62146989+cbarrete@users.noreply.github.com> Date: Sat, 27 Jun 2020 20:10:19 +0200 Subject: LSP: Set current name as default rename text (#12553) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since we don't know what the server considers to be a symbol, `cword` is the best bet in most cases. Co-authored-by: Cédric Barreteau <> --- runtime/lua/vim/lsp/buf.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 81d78c8b98..e1ddfb7efa 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -115,7 +115,7 @@ function M.rename(new_name) -- TODO(ashkan) use prepareRename -- * result: [`Range`](#range) \| `{ range: Range, placeholder: string }` \| `null` describing the range of the string to rename and optionally a placeholder text of the string content to be renamed. If `null` is returned then it is deemed that a 'textDocument/rename' request is not valid at the given position. local params = util.make_position_params() - new_name = new_name or npcall(vfn.input, "New Name: ") + new_name = new_name or npcall(vfn.input, "New Name: ", vfn.expand('')) if not (new_name and #new_name > 0) then return end params.newName = new_name request('textDocument/rename', params) -- cgit From 333f3f19db612acc893791f04624da174efe04b5 Mon Sep 17 00:00:00 2001 From: Thomas Vigouroux Date: Sun, 7 Jun 2020 16:37:11 +0200 Subject: treesitter: add set_included_ranges to the parser This is the first step towards language injection using treesitter. --- runtime/lua/vim/treesitter.lua | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua index c502e45bd0..d5bc8e0a1b 100644 --- a/runtime/lua/vim/treesitter.lua +++ b/runtime/lua/vim/treesitter.lua @@ -30,6 +30,12 @@ function Parser:_on_lines(bufnr, _, start_row, old_stop_row, stop_row, old_byte_ self.valid = false end +function Parser:set_included_ranges(ranges) + self._parser:set_included_ranges(self.bufnr, ranges) + -- The buffer will need to be parsed again later + self.valid = false +end + local M = { parse_query = vim._ts_parse_query, } -- cgit From b652f74ca3a6722ad0d185a0f4093907a6af65d7 Mon Sep 17 00:00:00 2001 From: Thomas Vigouroux Date: Tue, 16 Jun 2020 08:17:25 +0200 Subject: treesitter: use nodes to mark ranges --- runtime/lua/vim/treesitter.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua index d5bc8e0a1b..f356673839 100644 --- a/runtime/lua/vim/treesitter.lua +++ b/runtime/lua/vim/treesitter.lua @@ -31,7 +31,7 @@ function Parser:_on_lines(bufnr, _, start_row, old_stop_row, stop_row, old_byte_ end function Parser:set_included_ranges(ranges) - self._parser:set_included_ranges(self.bufnr, ranges) + self._parser:set_included_ranges(ranges) -- The buffer will need to be parsed again later self.valid = false end -- cgit From 554b21261ec0ad1eaac7afc21d9a1ba7fb7cabf1 Mon Sep 17 00:00:00 2001 From: Mathias Fußenegger Date: Tue, 30 Jun 2020 17:48:04 +0200 Subject: lsp: Use nvim_buf_get_lines in locations_to_items and add more tests (#12357) * LSP: Add tests & use nvim_buf_get_lines in locations_to_items This is to add support for cases where the server returns a URI in the locations that does not have a file scheme but needs to be loaded via a BufReadCmd event. * LSP: Don't iterate through all lines in locations_to_items * fixup! LSP: Don't iterate through all lines in locations_to_items * fixup! fixup! LSP: Don't iterate through all lines in locations_to_items * fixup! fixup! fixup! LSP: Don't iterate through all lines in locations_to_items --- runtime/lua/vim/lsp/util.lua | 43 +++++++++++++++++-------------------------- 1 file changed, 17 insertions(+), 26 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 4176255eb6..ab2992ba60 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -1089,40 +1089,31 @@ function M.locations_to_items(locations) for _, d in ipairs(locations) do -- locations may be Location or LocationLink local uri = d.uri or d.targetUri - local fname = assert(vim.uri_to_fname(uri)) local range = d.range or d.targetSelectionRange - table.insert(grouped[fname], {start = range.start}) + table.insert(grouped[uri], {start = range.start}) end local keys = vim.tbl_keys(grouped) table.sort(keys) -- TODO(ashkan) I wish we could do this lazily. - for _, fname in ipairs(keys) do - local rows = grouped[fname] - + for _, uri in ipairs(keys) do + local rows = grouped[uri] table.sort(rows, position_sort) - local i = 0 - for line in io.lines(fname) do - for _, temp in ipairs(rows) do - local pos = temp.start - local row = pos.line - if i == row then - local col - if pos.character > #line then - col = #line - else - col = vim.str_byteindex(line, pos.character) - end - table.insert(items, { - filename = fname, - lnum = row + 1, - col = col + 1; - text = line; - }) - end - end - i = i + 1 + local bufnr = vim.uri_to_bufnr(uri) + vim.fn.bufload(bufnr) + local filename = vim.uri_to_fname(uri) + for _, temp in ipairs(rows) do + local pos = temp.start + local row = pos.line + local line = (api.nvim_buf_get_lines(bufnr, row, row + 1, false) or {""})[1] + local col = M.character_offset(bufnr, row, pos.character) + table.insert(items, { + filename = filename, + lnum = row + 1, + col = col + 1; + text = line; + }) end end return items -- cgit From 7b529e7912517af078e005dd7b06b3d042be9cb7 Mon Sep 17 00:00:00 2001 From: TJ DeVries Date: Thu, 2 Jul 2020 07:09:17 -0400 Subject: doc: fix scripts and regenerate (#12506) * Fix some small doc issues * doc: fixup * doc: fixup * Fix lint and rebase * Remove bad advice * Ugh, stupid mpack files... * Don't let people include these for now until they specifically want to * Prevent duplicate tag --- runtime/lua/vim/lsp.lua | 10 +++++----- runtime/lua/vim/lsp/buf.lua | 13 +++++++++++++ runtime/lua/vim/lsp/rpc.lua | 6 ++++-- runtime/lua/vim/lsp/util.lua | 39 +++++++++++++++++++++++++++++++++++++-- runtime/lua/vim/shared.lua | 22 +++++++++++----------- 5 files changed, 70 insertions(+), 20 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index e759511347..7442f0c0b5 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -877,11 +877,11 @@ function lsp.buf_request_sync(bufnr, method, params, timeout_ms) end --- Send a notification to a server --- @param bufnr [number] (optional): The number of the buffer --- @param method [string]: Name of the request method --- @param params [string]: Arguments to send to the server --- --- @returns true if any client returns true; false otherwise +--@param bufnr [number] (optional): The number of the buffer +--@param method [string]: Name of the request method +--@param params [string]: Arguments to send to the server +--- +--@returns true if any client returns true; false otherwise function lsp.buf_notify(bufnr, method, params) validate { bufnr = { bufnr, 'n', true }; diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index e1ddfb7efa..839e00c67d 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -23,6 +23,9 @@ local function request(method, params, callback) return vim.lsp.buf_request(0, method, params, callback) end +--- Sends a notification through all clients associated with current buffer. +-- +--@return `true` if server responds. function M.server_ready() return not not vim.lsp.buf_notify(0, "window/progress", {}) end @@ -69,6 +72,10 @@ function M.formatting(options) return request('textDocument/formatting', params) end +--- Perform |vim.lsp.buf.formatting()| synchronously. +--- +--- Useful for running on save, to make sure buffer is formatted prior to being +--- saved. {timeout_ms} is passed on to |vim.lsp.buf_request_sync()|. function M.formatting_sync(options, timeout_ms) local params = util.make_formatting_params(options) local result = vim.lsp.buf_request_sync(0, "textDocument/formatting", params, timeout_ms) @@ -136,6 +143,12 @@ function M.document_symbol() request('textDocument/documentSymbol', params) end + +--- Lists all symbols in the current workspace in the quickfix window. +--- +--- The list is filtered against the optional argument {query}; +--- if the argument is omitted from the call, the user is prompted to enter a string on the command line. +--- An empty string means no filtering is done. function M.workspace_symbol(query) query = query or npcall(vfn.input, "Query: ") local params = {query = query} diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua index dad1dc11f1..81c92bfe05 100644 --- a/runtime/lua/vim/lsp/rpc.lua +++ b/runtime/lua/vim/lsp/rpc.lua @@ -36,10 +36,12 @@ end --- Merges current process env with the given env and returns the result as --- a list of "k=v" strings. --- +---
 --- Example:
 ---
----      { PRODUCTION="false", PATH="/usr/bin/", PORT=123, HOST="0.0.0.0", }
----   => { "PRODUCTION=false", "PATH=/usr/bin/", "PORT=123", "HOST=0.0.0.0", }
+---  in:    { PRODUCTION="false", PATH="/usr/bin/", PORT=123, HOST="0.0.0.0", }
+---  out:   { "PRODUCTION=false", "PATH=/usr/bin/", "PORT=123", "HOST=0.0.0.0", }
+--- 
local function env_merge(env) if env == nil then return env diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index ab2992ba60..6b19d3ecd6 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -952,7 +952,9 @@ do end --- Saves the diagnostics (Diagnostic[]) into diagnostics_by_buf - -- + --- + --@param bufnr bufnr for which the diagnostics are for. + --@param diagnostics Diagnostics[] received from the language server. function M.buf_diagnostics_save_positions(bufnr, diagnostics) validate { bufnr = {bufnr, 'n', true}; @@ -1044,6 +1046,29 @@ do end end + --- Returns the number of diagnostics of given kind for current buffer. + --- + --- Useful for showing diagnostic counts in statusline. eg: + --- + ---
+  --- function! LspStatus() abort
+  ---     let sl = ''
+  ---     if luaeval('not vim.tbl_isempty(vim.lsp.buf_get_clients(0))')
+  ---         let sl.='%#MyStatuslineLSP#E:'
+  ---         let sl.='%#MyStatuslineLSPErrors#%{luaeval("vim.lsp.util.buf_diagnostics_count([[Error]])")}'
+  ---         let sl.='%#MyStatuslineLSP# W:'
+  ---         let sl.='%#MyStatuslineLSPWarnings#%{luaeval("vim.lsp.util.buf_diagnostics_count([[Warning]])")}'
+  ---     else
+  ---         let sl.='%#MyStatuslineLSPErrors#off'
+  ---     endif
+  ---     return sl
+  --- endfunction
+  --- let &l:statusline = '%#MyStatuslineLSP#LSP '.LspStatus()
+  --- 
+ --- + --@param kind Diagnostic severity kind: See |vim.lsp.protocol.DiagnosticSeverity| + --- + --@return Count of diagnostics function M.buf_diagnostics_count(kind) local bufnr = vim.api.nvim_get_current_buf() local diagnostics = M.diagnostics_by_buf[bufnr] @@ -1064,6 +1089,16 @@ do [protocol.DiagnosticSeverity.Hint] = "LspDiagnosticsHintSign"; } + --- Place signs for each diagnostic in the sign column. + --- + --- Sign characters can be customized with the following commands: + --- + ---
+  --- sign define LspDiagnosticsErrorSign text=E texthl=LspDiagnosticsError linehl= numhl=
+  --- sign define LspDiagnosticsWarningSign text=W texthl=LspDiagnosticsWarning linehl= numhl=
+  --- sign define LspDiagnosticsInformationSign text=I texthl=LspDiagnosticsInformation linehl= numhl=
+  --- sign define LspDiagnosticsHintSign text=H texthl=LspDiagnosticsHint linehl= numhl=
+  --- 
function M.buf_diagnostics_signs(bufnr, diagnostics) for _, diagnostic in ipairs(diagnostics) do vim.fn.sign_place(0, sign_ns, diagnostic_severity_map[diagnostic.severity], bufnr, {lnum=(diagnostic.range.start.line+1)}) @@ -1142,7 +1177,7 @@ end --- Convert symbols to quickfix list items --- ---@symbols DocumentSymbol[] or SymbolInformation[] +--@param symbols DocumentSymbol[] or SymbolInformation[] function M.symbols_to_items(symbols, bufnr) local function _symbols_to_items(_symbols, _items, _bufnr) for _, symbol in ipairs(_symbols) do diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 384d22cb89..6e427665f2 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -347,16 +347,16 @@ function vim.tbl_flatten(t) return result end --- Determine whether a Lua table can be treated as an array. --- --- An empty table `{}` will default to being treated as an array. --- Use `vim.emtpy_dict()` to create a table treated as an --- empty dict. Empty tables returned by `rpcrequest()` and --- `vim.fn` functions can be checked using this function --- whether they represent empty API arrays and vimL lists. ---- ---@params Table ---@returns true: An array-like table, false: A dict-like or mixed table +--- Determine whether a Lua table can be treated as an array. +--- +--- An empty table `{}` will default to being treated as an array. +--- Use `vim.emtpy_dict()` to create a table treated as an +--- empty dict. Empty tables returned by `rpcrequest()` and +--- `vim.fn` functions can be checked using this function +--- whether they represent empty API arrays and vimL lists. +--- +--@param t Table +--@returns `true` if array-like table, else `false`. function vim.tbl_islist(t) if type(t) ~= 'table' then return false @@ -392,7 +392,7 @@ end --- --- --@see https://github.com/Tieske/Penlight/blob/master/lua/pl/tablex.lua ---@param Table +--@param t Table --@returns Number that is the number of the value in table function vim.tbl_count(t) vim.validate{t={t,'t'}} -- cgit From f9579d473e00c11c0b76ef5fe0934ef6ec13ed34 Mon Sep 17 00:00:00 2001 From: Christian Clason Date: Mon, 6 Jul 2020 03:09:52 +0200 Subject: lsp: add optional vertical padding, maximal size to floats (#12444) * add vertical padding to floats * add max_width, max_height option to float methods * lint * lintlint * guard against nil wrap_at --- runtime/lua/vim/lsp/util.lua | 49 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 9 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 6b19d3ecd6..52a6fe89f3 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -619,8 +619,10 @@ end --- --@param contents table of lines to trim and pad --@param opts dictionary with optional fields --- - pad_left amount of columns to pad contents at left (default 1) --- - pad_right amount of columns to pad contents at right (default 1) +-- - pad_left number of columns to pad contents at left (default 1) +-- - pad_right number of columns to pad contents at right (default 1) +-- - pad_top number of lines to pad contents at top (default 0) +-- - pad_bottom number of lines to pad contents at bottom (default 0) --@return contents table of trimmed and padded lines function M._trim_and_pad(contents, opts) validate { @@ -634,6 +636,16 @@ function M._trim_and_pad(contents, opts) for i, line in ipairs(contents) do contents[i] = string.format('%s%s%s', left_padding, line:gsub("\r", ""), right_padding) end + if opts.pad_top then + for _ = 1, opts.pad_top do + table.insert(contents, 1, "") + end + end + if opts.pad_bottom then + for _ = 1, opts.pad_bottom do + table.insert(contents, "") + end + end return contents end @@ -651,8 +663,12 @@ end -- - height of floating window -- - width of floating window -- - wrap_at character to wrap at for computing height --- - pad_left amount of columns to pad contents at left --- - pad_right amount of columns to pad contents at right +-- - max_width maximal width of floating window +-- - max_height maximal height of floating window +-- - pad_left number of columns to pad contents at left +-- - pad_right number of columns to pad contents at right +-- - pad_top number of lines to pad contents at top +-- - pad_bottom number of lines to pad contents at bottom -- - separator insert separator after code block --@return width,height size of float function M.fancy_floating_markdown(contents, opts) @@ -763,6 +779,8 @@ end -- - height of floating window -- - width of floating window -- - wrap_at character to wrap at for computing height +-- - max_width maximal width of floating window +-- - max_height maximal height of floating window --@return width,height size of float function M._make_floating_popup_size(contents, opts) validate { @@ -773,6 +791,9 @@ function M._make_floating_popup_size(contents, opts) local width = opts.width local height = opts.height + local wrap_at = opts.wrap_at + local max_width = opts.max_width + local max_height = opts.max_height local line_widths = {} if not width then @@ -783,11 +804,14 @@ function M._make_floating_popup_size(contents, opts) width = math.max(line_widths[i], width) end end + if max_width then + width = math.min(width, max_width) + wrap_at = math.min(wrap_at or max_width, max_width) + end if not height then height = #contents - local wrap_at = opts.wrap_at - if wrap_at and width > wrap_at then + if wrap_at and width >= wrap_at then height = 0 if vim.tbl_isempty(line_widths) then for _, line in ipairs(contents) do @@ -796,11 +820,14 @@ function M._make_floating_popup_size(contents, opts) end else for i = 1, #contents do - height = height + math.ceil(line_widths[i]/wrap_at) + height = height + math.max(1, math.ceil(line_widths[i]/wrap_at)) end end end end + if max_height then + height = math.min(height, max_height) + end return width, height end @@ -813,8 +840,12 @@ end -- - height of floating window -- - width of floating window -- - wrap_at character to wrap at for computing height --- - pad_left amount of columns to pad contents at left --- - pad_right amount of columns to pad contents at right +-- - max_width maximal width of floating window +-- - max_height maximal height of floating window +-- - pad_left number of columns to pad contents at left +-- - pad_right number of columns to pad contents at right +-- - pad_top number of lines to pad contents at top +-- - pad_bottom number of lines to pad contents at bottom --@return bufnr,winnr buffer and window number of floating window or nil function M.open_floating_preview(contents, filetype, opts) validate { -- cgit From 4ab7bbf3eaeacc32e8970b76a19c8682f98cc183 Mon Sep 17 00:00:00 2001 From: Christian Clason Date: Mon, 6 Jul 2020 03:30:12 +0200 Subject: lua: add options to highlight.on_yank (#12549) NOTE: Configuration options have changed for highlight.on_yank. Check help for |:help highlight.on_yank()| --- runtime/lua/vim/highlight.lua | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/highlight.lua b/runtime/lua/vim/highlight.lua index 69c3c8a4dc..ce0a3de520 100644 --- a/runtime/lua/vim/highlight.lua +++ b/runtime/lua/vim/highlight.lua @@ -23,24 +23,41 @@ function highlight.range(bufnr, ns, higroup, start, finish, rtype, inclusive) end +local yank_ns = api.nvim_create_namespace('hlyank') --- Highlight the yanked region --- --- use from init.vim via ---- au TextYankPost * lua require'vim.highlight'.on_yank() +--- au TextYankPost * lua vim.highlight.on_yank() --- customize highlight group and timeout via ---- au TextYankPost * lua require'vim.highlight'.on_yank("IncSearch", 500) +--- au TextYankPost * lua vim.highlight.on_yank {higroup="IncSearch", timeout=150} +--- customize conditions (here: do not highlight a visual selection) via +--- au TextYankPost * lua vim.highlight.on_yank {on_visual=false} --- --- @param higroup highlight group for yanked region --- @param timeout time in ms before highlight is cleared --- @param event event structure -function highlight.on_yank(higroup, timeout, event) - event = event or vim.v.event +-- @param opts dictionary with options controlling the highlight: +-- - higroup highlight group for yanked region (default "IncSearch") +-- - timeout time in ms before highlight is cleared (default 150) +-- - on_macro highlight when executing macro (default false) +-- - on_visual highlight when yanking visual selection (default true) +-- - event event structure (default vim.v.event) +function highlight.on_yank(opts) + vim.validate { + opts = { opts, + function(t) if t == nil then return true else return type(t) == 'table' end end, + 'a table or nil to configure options (see `:h highlight.on_yank`)', + }} + opts = opts or {} + local event = opts.event or vim.v.event + local on_macro = opts.on_macro or false + local on_visual = (opts.on_visual ~= false) + + if (not on_macro) and vim.fn.reg_executing() ~= '' then return end if event.operator ~= 'y' or event.regtype == '' then return end - higroup = higroup or "IncSearch" - timeout = timeout or 500 + if (not on_visual) and event.visual then return end + + local higroup = opts.higroup or "IncSearch" + local timeout = opts.timeout or 150 local bufnr = api.nvim_get_current_buf() - local yank_ns = api.nvim_create_namespace('') api.nvim_buf_clear_namespace(bufnr, yank_ns, 0, -1) local pos1 = vim.fn.getpos("'[") -- cgit From 529251d5e49ed869aa9b4b3a168e97c8e30211d6 Mon Sep 17 00:00:00 2001 From: Thomas Vigouroux <39092278+vigoux@users.noreply.github.com> Date: Fri, 10 Jul 2020 15:33:27 +0200 Subject: treesitter: call bufload before parsing (#12603) --- runtime/lua/vim/treesitter.lua | 3 +++ 1 file changed, 3 insertions(+) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua index f356673839..edff94af0b 100644 --- a/runtime/lua/vim/treesitter.lua +++ b/runtime/lua/vim/treesitter.lua @@ -75,6 +75,9 @@ function M.create_parser(bufnr, lang, id) if bufnr == 0 then bufnr = a.nvim_get_current_buf() end + + vim.fn.bufload(bufnr) + local self = setmetatable({bufnr=bufnr, lang=lang, valid=false}, Parser) self._parser = vim._create_ts_parser(lang) self.change_cbs = {} -- cgit From c5119dbad633752568ea53596d2f483ee63c1ce8 Mon Sep 17 00:00:00 2001 From: Thomas Vigouroux Date: Fri, 3 Jul 2020 19:16:40 +0200 Subject: treesitter: use change calbacks on redraw --- runtime/lua/vim/tshighlighter.lua | 87 ++++++++++++++------------------------- 1 file changed, 30 insertions(+), 57 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/tshighlighter.lua b/runtime/lua/vim/tshighlighter.lua index 1440acf0d0..c1e77ea792 100644 --- a/runtime/lua/vim/tshighlighter.lua +++ b/runtime/lua/vim/tshighlighter.lua @@ -3,6 +3,7 @@ local a = vim.api -- support reload for quick experimentation local TSHighlighter = rawget(vim.treesitter, 'TSHighlighter') or {} TSHighlighter.__index = TSHighlighter +local ts_hs_ns = a.nvim_create_namespace("treesitter_hl") -- These are conventions defined by tree-sitter, though it -- needs to be user extensible also. @@ -26,7 +27,7 @@ function TSHighlighter.new(query, bufnr, ft) local self = setmetatable({}, TSHighlighter) self.parser = vim.treesitter.get_parser(bufnr, ft, function(...) self:on_change(...) end) self.buf = self.parser.bufnr - -- TODO(bfredl): perhaps on_start should be called uncondionally, instead for only on mod? + local tree = self.parser:parse() self.root = tree:root() self:set_query(query) @@ -34,10 +35,8 @@ function TSHighlighter.new(query, bufnr, ft) self.redraw_count = 0 self.line_count = {} a.nvim_buf_set_option(self.buf, "syntax", "") - a.nvim__buf_set_luahl(self.buf, { - on_start=function(...) return self:on_start(...) end, - on_window=function(...) return self:on_window(...) end, - on_line=function(...) return self:on_line(...) end, + a.nvim_buf_attach(self.buf, false, { + on_lines=function(_) self.root = self.parser:parse():root() end }) -- Tricky: if syntax hasn't been enabled, we need to reload color scheme @@ -47,6 +46,7 @@ function TSHighlighter.new(query, bufnr, ft) if vim.g.syntax_on ~= 1 then vim.api.nvim_command("runtime! syntax/synload.vim") end + return self end @@ -56,67 +56,40 @@ function TSHighlighter:set_query(query) end self.query = query - self.id_map = {} - for i, capture in ipairs(self.query.captures) do - local hl = 0 - local firstc = string.sub(capture, 1, 1) - local hl_group = self.hl_map[capture] - if firstc ~= string.lower(firstc) then - hl_group = vim.split(capture, '.', true)[1] - end - if hl_group then - hl = a.nvim_get_hl_id_by_name(hl_group) - end - self.id_map[i] = hl - end - - a.nvim__buf_redraw_range(self.buf, 0, a.nvim_buf_line_count(self.buf)) + self:on_change({{self.root:range()}}) end function TSHighlighter:on_change(changes) + -- Get a fresh root + self.root = self.parser.tree:root() + for _, ch in ipairs(changes or {}) do - a.nvim__buf_redraw_range(self.buf, ch[1], ch[3]+1) - end - self.edit_count = self.edit_count + 1 -end + -- Try to be as exact as possible + local changed_node = self.root:descendant_for_range(ch[1], ch[2], ch[3], ch[4]) -function TSHighlighter:on_start(_, _buf, _tick) - local tree = self.parser:parse() - self.root = tree:root() -end + a.nvim_buf_clear_namespace(self.buf, ts_hs_ns, ch[1], ch[3]) -function TSHighlighter:on_window(_, _win, _buf, _topline, botline) - self.iter = nil - self.nextrow = 0 - self.botline = botline - self.redraw_count = self.redraw_count + 1 -end + for capture, node in self.query:iter_captures(changed_node, self.buf, ch[1], ch[3] + 1) do + local start_row, start_col, end_row, end_col = node:range() + local capture_name = self.query.captures[capture] -function TSHighlighter:on_line(_, _win, buf, line) - if self.iter == nil then - self.iter = self.query:iter_captures(self.root,buf,line,self.botline) - end - while line >= self.nextrow do - local capture, node, match = self.iter() - local active = true - if capture == nil then - break - end - if match ~= nil then - active = self:run_pred(match) - match.active = active - end - local start_row, start_col, end_row, end_col = node:range() - local hl = self.id_map[capture] - if hl > 0 and active and end_row >= line then - a.nvim__put_attr(hl, start_row, start_col, end_row, end_col) - end - if start_row > line then - self.nextrow = start_row + local firstc = string.sub(capture_name, 1, 1) + local hl + -- TODO(vigoux): maybe we want to cache the capture -> highlight relation + if firstc ~= string.lower(firstc) then + hl = vim.split(capture_name, '.', true)[1] + else + hl = TSHighlighter.hl_map[capture_name] + end + + if hl then + a.nvim__buf_add_decoration(self.buf, ts_hs_ns, hl, + start_row, start_col, + end_row, end_col, + {}) + end end end - self.line_count[line] = (self.line_count[line] or 0) + 1 - --return tostring(self.line_count[line]) end return TSHighlighter -- cgit From 0f7eaa35551f979f4458e7a71fa14f9ef987807c Mon Sep 17 00:00:00 2001 From: Thomas Vigouroux Date: Mon, 6 Jul 2020 22:11:30 +0200 Subject: treesitter: cache the capture hl relation --- runtime/lua/vim/tshighlighter.lua | 40 +++++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 12 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/tshighlighter.lua b/runtime/lua/vim/tshighlighter.lua index c1e77ea792..a0ff53cc00 100644 --- a/runtime/lua/vim/tshighlighter.lua +++ b/runtime/lua/vim/tshighlighter.lua @@ -46,16 +46,42 @@ function TSHighlighter.new(query, bufnr, ft) if vim.g.syntax_on ~= 1 then vim.api.nvim_command("runtime! syntax/synload.vim") end - return self end +local function is_highlight_name(capture_name) + local firstc = string.sub(capture_name, 1, 1) + return firstc ~= string.lower(firstc) +end + +function TSHighlighter:get_hl_from_capture(capture) + + local name = self.query.captures[capture] + + if is_highlight_name(name) then + -- From "Normal.left" only keep "Normal" + return vim.split(name, '.', true)[1] + else + -- Default to false to avoid recomputing + return TSHighlighter.hl_map[name] + end +end + function TSHighlighter:set_query(query) if type(query) == "string" then query = vim.treesitter.parse_query(self.parser.lang, query) end self.query = query + self.hl_cache = setmetatable({}, { + __index = function(table, capture) + local hl = self:get_hl_from_capture(capture) + rawset(table, capture, hl) + + return hl + end + }) + self:on_change({{self.root:range()}}) end @@ -71,17 +97,7 @@ function TSHighlighter:on_change(changes) for capture, node in self.query:iter_captures(changed_node, self.buf, ch[1], ch[3] + 1) do local start_row, start_col, end_row, end_col = node:range() - local capture_name = self.query.captures[capture] - - local firstc = string.sub(capture_name, 1, 1) - local hl - -- TODO(vigoux): maybe we want to cache the capture -> highlight relation - if firstc ~= string.lower(firstc) then - hl = vim.split(capture_name, '.', true)[1] - else - hl = TSHighlighter.hl_map[capture_name] - end - + local hl = self.hl_cache[capture] if hl then a.nvim__buf_add_decoration(self.buf, ts_hs_ns, hl, start_row, start_col, -- cgit From 341e139992e7bcfe02f41575ac4a9450d33dae26 Mon Sep 17 00:00:00 2001 From: Thomas Vigouroux Date: Wed, 8 Jul 2020 22:47:57 +0200 Subject: treesitter: add parser on_lines callbacks --- runtime/lua/vim/treesitter.lua | 29 ++++++++++++++++++++++------- runtime/lua/vim/tshighlighter.lua | 17 +++++++++++------ 2 files changed, 33 insertions(+), 13 deletions(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua index edff94af0b..927456708c 100644 --- a/runtime/lua/vim/treesitter.lua +++ b/runtime/lua/vim/treesitter.lua @@ -15,19 +15,27 @@ function Parser:parse() local changes self.tree, changes = self._parser:parse_buf(self.bufnr) self.valid = true - for _, cb in ipairs(self.change_cbs) do - cb(changes) + + if not vim.tbl_isempty(changes) then + for _, cb in ipairs(self.changedtree_cbs) do + cb(changes) + end end + return self.tree, changes end -function Parser:_on_lines(bufnr, _, start_row, old_stop_row, stop_row, old_byte_size) +function Parser:_on_lines(bufnr, changed_tick, start_row, old_stop_row, stop_row, old_byte_size) local start_byte = a.nvim_buf_get_offset(bufnr,start_row) local stop_byte = a.nvim_buf_get_offset(bufnr,stop_row) local old_stop_byte = start_byte + old_byte_size self._parser:edit(start_byte,old_stop_byte,stop_byte, start_row,0,old_stop_row,0,stop_row,0) self.valid = false + + for _, cb in ipairs(self.lines_cbs) do + cb(bufnr, changed_tick, start_row, old_stop_row, stop_row, old_byte_size) + end end function Parser:set_included_ranges(ranges) @@ -80,7 +88,8 @@ function M.create_parser(bufnr, lang, id) local self = setmetatable({bufnr=bufnr, lang=lang, valid=false}, Parser) self._parser = vim._create_ts_parser(lang) - self.change_cbs = {} + self.changedtree_cbs = {} + self.lines_cbs = {} self:parse() -- TODO(bfredl): use weakref to self, so that the parser is free'd is no plugin is -- using it. @@ -99,7 +108,7 @@ function M.create_parser(bufnr, lang, id) return self end -function M.get_parser(bufnr, ft, cb) +function M.get_parser(bufnr, ft, buf_attach_cbs) if bufnr == nil or bufnr == 0 then bufnr = a.nvim_get_current_buf() end @@ -111,9 +120,15 @@ function M.get_parser(bufnr, ft, cb) if parsers[id] == nil then parsers[id] = M.create_parser(bufnr, ft, id) end - if cb ~= nil then - table.insert(parsers[id].change_cbs, cb) + + if buf_attach_cbs and buf_attach_cbs.on_changedtree then + table.insert(parsers[id].changedtree_cbs, buf_attach_cbs.on_changedtree) + end + + if buf_attach_cbs and buf_attach_cbs.on_lines then + table.insert(parsers[id].lines_cbs, buf_attach_cbs.on_lines) end + return parsers[id] end diff --git a/runtime/lua/vim/tshighlighter.lua b/runtime/lua/vim/tshighlighter.lua index a0ff53cc00..6465751ae8 100644 --- a/runtime/lua/vim/tshighlighter.lua +++ b/runtime/lua/vim/tshighlighter.lua @@ -25,7 +25,15 @@ TSHighlighter.hl_map = { function TSHighlighter.new(query, bufnr, ft) local self = setmetatable({}, TSHighlighter) - self.parser = vim.treesitter.get_parser(bufnr, ft, function(...) self:on_change(...) end) + self.parser = vim.treesitter.get_parser( + bufnr, + ft, + { + on_changedtree = function(...) self:on_changedtree(...) end, + on_lines = function() self.root = self.parser:parse():root() end + } + ) + self.buf = self.parser.bufnr local tree = self.parser:parse() @@ -35,9 +43,6 @@ function TSHighlighter.new(query, bufnr, ft) self.redraw_count = 0 self.line_count = {} a.nvim_buf_set_option(self.buf, "syntax", "") - a.nvim_buf_attach(self.buf, false, { - on_lines=function(_) self.root = self.parser:parse():root() end - }) -- Tricky: if syntax hasn't been enabled, we need to reload color scheme -- but use synload.vim rather than syntax.vim to not enable @@ -82,10 +87,10 @@ function TSHighlighter:set_query(query) end }) - self:on_change({{self.root:range()}}) + self:on_changedtree({{self.root:range()}}) end -function TSHighlighter:on_change(changes) +function TSHighlighter:on_changedtree(changes) -- Get a fresh root self.root = self.parser.tree:root() -- cgit From 08efa7037e05ce229150d17db11b1b1c2419631f Mon Sep 17 00:00:00 2001 From: cbarrete <62146989+cbarrete@users.noreply.github.com> Date: Sat, 18 Jul 2020 21:10:09 +0200 Subject: lsp: Add support for call hierarchies (#12556) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * LSP: Add support for call hierarchies * LSP: Add support for call hierarchies * LSP: Add support for call hierarchies * LSP: Jump to call location Jump to the call site instead of jumping to the definition of the caller/callee. * LSP: add tests for the call hierarchy callbacks * Fix linting error Co-authored-by: Cédric Barreteau <> --- runtime/lua/vim/lsp.lua | 1 + runtime/lua/vim/lsp/buf.lua | 32 ++++++++++++++++++++++++++++++++ runtime/lua/vim/lsp/callbacks.lua | 27 +++++++++++++++++++++++++++ runtime/lua/vim/lsp/protocol.lua | 4 ++++ 4 files changed, 64 insertions(+) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 7442f0c0b5..6fe1d15b7e 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -511,6 +511,7 @@ function lsp.start_client(config) or (not client.resolved_capabilities.type_definition and method == 'textDocument/typeDefinition') or (not client.resolved_capabilities.document_symbol and method == 'textDocument/documentSymbol') or (not client.resolved_capabilities.workspace_symbol and method == 'textDocument/workspaceSymbol') + or (not client.resolved_capabilities.call_hierarchy and method == 'textDocument/prepareCallHierarchy') then callback(unsupported_method(method), method, nil, client_id, bufnr) return diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 839e00c67d..476bb3ba6f 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -143,6 +143,38 @@ function M.document_symbol() request('textDocument/documentSymbol', params) end +local function pick_call_hierarchy_item(call_hierarchy_items) + if not call_hierarchy_items then return end + if #call_hierarchy_items == 1 then + return call_hierarchy_items[1] + end + local items = {} + for i, item in ipairs(call_hierarchy_items) do + local entry = item.detail or item.name + table.insert(items, string.format("%d. %s", i, entry)) + end + local choice = vim.fn.inputlist(items) + if choice < 1 or choice > #items then + return + end + return choice +end + +function M.incoming_calls() + local params = util.make_position_params() + request('textDocument/prepareCallHierarchy', params, function(_, _, result) + local call_hierarchy_item = pick_call_hierarchy_item(result) + vim.lsp.buf_request(0, 'callHierarchy/incomingCalls', { item = call_hierarchy_item }) + end) +end + +function M.outgoing_calls() + local params = util.make_position_params() + request('textDocument/prepareCallHierarchy', params, function(_, _, result) + local call_hierarchy_item = pick_call_hierarchy_item(result) + vim.lsp.buf_request(0, 'callHierarchy/outgoingCalls', { item = call_hierarchy_item }) + end) +end --- Lists all symbols in the current workspace in the quickfix window. --- diff --git a/runtime/lua/vim/lsp/callbacks.lua b/runtime/lua/vim/lsp/callbacks.lua index 4b14f0132d..1ed58995d0 100644 --- a/runtime/lua/vim/lsp/callbacks.lua +++ b/runtime/lua/vim/lsp/callbacks.lua @@ -214,6 +214,33 @@ M['textDocument/documentHighlight'] = function(_, _, result, _) util.buf_highlight_references(bufnr, result) end +-- direction is "from" for incoming calls and "to" for outgoing calls +local make_call_hierarchy_callback = function(direction) + -- result is a CallHierarchy{Incoming,Outgoing}Call[] + return function(_, _, result) + if not result then return end + local items = {} + for _, call_hierarchy_call in pairs(result) do + local call_hierarchy_item = call_hierarchy_call[direction] + for _, range in pairs(call_hierarchy_call.fromRanges) do + table.insert(items, { + filename = assert(vim.uri_to_fname(call_hierarchy_item.uri)), + text = call_hierarchy_item.name, + lnum = range.start.line + 1, + col = range.start.character + 1, + }) + end + end + util.set_qflist(items) + api.nvim_command("copen") + api.nvim_command("wincmd p") + end +end + +M['callHierarchy/incomingCalls'] = make_call_hierarchy_callback('from') + +M['callHierarchy/outgoingCalls'] = make_call_hierarchy_callback('to') + M['window/logMessage'] = function(_, _, result, client_id) local message_type = result.type local message = result.message diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index 4fded1961d..ef5e08680e 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -713,6 +713,9 @@ function protocol.make_client_capabilities() }; applyEdit = true; }; + callHierarchy = { + dynamicRegistration = false; + }; experimental = nil; } end @@ -912,6 +915,7 @@ function protocol.resolve_capabilities(server_capabilities) general_properties.workspace_symbol = server_capabilities.workspaceSymbolProvider or false general_properties.document_formatting = server_capabilities.documentFormattingProvider or false general_properties.document_range_formatting = server_capabilities.documentRangeFormattingProvider or false + general_properties.call_hierarchy = server_capabilities.callHierarchyProvider or false if server_capabilities.codeActionProvider == nil then general_properties.code_action = false -- cgit From 56f3b95180ba011d9228c30c44eee9a9ab0fef84 Mon Sep 17 00:00:00 2001 From: cbarrete <62146989+cbarrete@users.noreply.github.com> Date: Sun, 19 Jul 2020 23:16:12 +0200 Subject: doc: Add documentation for some `vim.lsp.buf` functions (#12552) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add documentation for some `vim.lsp.buf` functions * Add inline Lua documentation * Use generated documentation for LSP buffer functions Co-authored-by: Cédric Barreteau <> --- runtime/lua/vim/lsp/buf.lua | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 476bb3ba6f..2e27617997 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -30,43 +30,63 @@ function M.server_ready() return not not vim.lsp.buf_notify(0, "window/progress", {}) end +--- Displays hover information about the symbol under the cursor in a floating +--- window. Calling the function twice will jump into the floating window. function M.hover() local params = util.make_position_params() request('textDocument/hover', params) end +--- Jumps to the declaration of the symbol under the cursor. +--- function M.declaration() local params = util.make_position_params() request('textDocument/declaration', params) end +--- Jumps to the definition of the symbol under the cursor. +--- function M.definition() local params = util.make_position_params() request('textDocument/definition', params) end +--- Jumps to the definition of the type of the symbol under the cursor. +--- function M.type_definition() local params = util.make_position_params() request('textDocument/typeDefinition', params) end +--- Lists all the implementations for the symbol under the cursor in the +--- quickfix window. function M.implementation() local params = util.make_position_params() request('textDocument/implementation', params) end +--- Displays signature information about the symbol under the cursor in a +--- floating window. function M.signature_help() local params = util.make_position_params() request('textDocument/signatureHelp', params) end --- TODO(ashkan) ? +--- Retrieves the completion items at the current cursor position. Can only be +--- called in Insert mode. function M.completion(context) local params = util.make_position_params() params.context = context return request('textDocument/completion', params) end +--- Formats the current buffer. +--- +--- The optional {options} table can be used to specify FormattingOptions, a +--- list of which is available at +--- https://microsoft.github.io/language-server-protocol/specification#textDocument_formatting. +--- Some unspecified options will be automatically derived from the current +--- Neovim options. function M.formatting(options) local params = util.make_formatting_params(options) return request('textDocument/formatting', params) @@ -118,6 +138,8 @@ function M.range_formatting(options, start_pos, end_pos) return request('textDocument/rangeFormatting', params) end +--- Renames all references to the symbol under the cursor. If {new_name} is not +--- provided, the user will be prompted for a new name using |input()|. function M.rename(new_name) -- TODO(ashkan) use prepareRename -- * result: [`Range`](#range) \| `{ range: Range, placeholder: string }` \| `null` describing the range of the string to rename and optionally a placeholder text of the string content to be renamed. If `null` is returned then it is deemed that a 'textDocument/rename' request is not valid at the given position. @@ -128,6 +150,8 @@ function M.rename(new_name) request('textDocument/rename', params) end +--- Lists all the references to the symbol under the cursor in the quickfix window. +--- function M.references(context) validate { context = { context, 't', true } } local params = util.make_position_params() @@ -138,6 +162,8 @@ function M.references(context) request('textDocument/references', params) end +--- Lists all symbols in the current buffer in the quickfix window. +--- function M.document_symbol() local params = { textDocument = util.make_text_document_params() } request('textDocument/documentSymbol', params) -- cgit From 8bb2c3087a228de0aa95a6d19ae61d93ffe70f62 Mon Sep 17 00:00:00 2001 From: Cédric Barreteau <> Date: Fri, 17 Jul 2020 20:51:51 +0200 Subject: LSP: make the hover window nomodifiable --- runtime/lua/vim/lsp/util.lua | 1 + 1 file changed, 1 insertion(+) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 52a6fe89f3..cf084dc060 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -594,6 +594,7 @@ function M.focusable_float(unique_name, fn) if win then api.nvim_set_current_win(win) api.nvim_command("stopinsert") + api.nvim_buf_set_option(0, 'modifiable', false) return end end -- cgit From fd6361278b79bb3ce9462dcedd0689ad9accd66c Mon Sep 17 00:00:00 2001 From: Cédric Barreteau <> Date: Sun, 19 Jul 2020 17:36:04 +0200 Subject: Make the window `nomodifiable` when it's created --- runtime/lua/vim/lsp/util.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index cf084dc060..5a68138f1e 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -594,7 +594,6 @@ function M.focusable_float(unique_name, fn) if win then api.nvim_set_current_win(win) api.nvim_command("stopinsert") - api.nvim_buf_set_option(0, 'modifiable', false) return end end @@ -736,6 +735,7 @@ function M.fancy_floating_markdown(contents, opts) local bufnr = api.nvim_create_buf(false, true) local winnr = api.nvim_open_win(bufnr, false, M.make_floating_popup_options(width, height, opts)) vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, stripped) + api.nvim_buf_set_option(bufnr, 'modifiable', false) -- Switch to the floating window to apply the syntax highlighting. -- This is because the syntax command doesn't accept a target. -- cgit From 82bfdbfe5c4eebd98ef59b52045ffd198e7ff389 Mon Sep 17 00:00:00 2001 From: Andreas Johansson Date: Thu, 30 Jul 2020 19:37:19 +0200 Subject: Revert "lsp: Fix text edits with the same start position (#12434)" (#12564) This reverts commit 44fe8828f06a22bc9aa3617a6fd8aae447a838de. --- runtime/lua/vim/lsp/util.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 5a68138f1e..d286f28d0c 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -92,7 +92,7 @@ local function sort_by_key(fn) end end local edit_sort_key = sort_by_key(function(e) - return {e.A[1], e.A[2], -e.i} + return {e.A[1], e.A[2], e.i} end) --- Position is a https://microsoft.github.io/language-server-protocol/specifications/specification-current/#position -- cgit