diff --git a/lua/symbols-outline/markdown.lua b/lua/symbols-outline/markdown.lua index cd909fc..e035a55 100644 --- a/lua/symbols-outline/markdown.lua +++ b/lua/symbols-outline/markdown.lua @@ -62,7 +62,7 @@ function M.handle_markdown() end end - return { [1000000] = { result = level_symbols[1].children } } + return level_symbols[1].children end return M diff --git a/lua/symbols-outline/parser.lua b/lua/symbols-outline/parser.lua index fe2f8d1..50ad125 100644 --- a/lua/symbols-outline/parser.lua +++ b/lua/symbols-outline/parser.lua @@ -2,6 +2,7 @@ local symbols = require 'symbols-outline.symbols' local ui = require 'symbols-outline.ui' local config = require 'symbols-outline.config' local t_utils = require 'symbols-outline.utils.table' +local lsp_utils = require 'symbols-outline.utils.lsp_utils' local folding = require 'symbols-outline.folding' local M = {} @@ -26,17 +27,8 @@ local function parse_result(result, depth, hierarchy, parent) -- whether this node is the last in its group local isLast = index == #result - -- support SymbolInformation[] - -- https://microsoft.github.io/language-server-protocol/specification#textDocument_documentSymbol - local selectionRange = value.selectionRange - if value.selectionRange == nil then - selectionRange = value.location.range - end - - local range = value.range - if value.range == nil then - range = value.location.range - end + local selectionRange = lsp_utils.get_selection_range(value) + local range = lsp_utils.get_range(value) local node = { deprecated = value.deprecated, @@ -70,77 +62,11 @@ local function parse_result(result, depth, hierarchy, parent) return ret end ----Sorts the result from LSP by where the symbols start. ----@param result table Result containing symbols returned from textDocument/documentSymbol ----@return table -local function sort_result(result) - ---Returns the start location for a symbol, or nil if not found. - ---@param item table The symbol. - ---@return table|nil - local function get_range_start(item) - if item.location ~= nil then - return item.location.range.start - elseif item.range ~= nil then - return item.range.start - else - return nil - end - end - - table.sort(result, function(a, b) - local a_start = get_range_start(a) - local b_start = get_range_start(b) - - -- if they both are equal, a should be before b - if a_start == nil and b_start == nil then - return false - end - - -- those with no start go first - if a_start == nil then - return true - end - if b_start == nil then - return false - end - - -- first try to sort by line. If lines are equal, sort by character instead - if a_start.line ~= b_start.line then - return a_start.line < b_start.line - else - return a_start.character < b_start.character - end - end) - - return result -end - ---Parses the response from lsp request 'textDocument/documentSymbol' using buf_request_all ---@param response table The result from buf_request_all ---@return table outline items function M.parse(response) - local all_results = {} - - -- flatten results to one giant table of symbols - for client_id, client_response in pairs(response) do - if config.is_client_blacklisted(client_id) then - print('skipping client ' .. client_id) - goto continue - end - - local result = client_response['result'] - if result == nil or type(result) ~= 'table' then - goto continue - end - - for _, value in pairs(result) do - table.insert(all_results, value) - end - - ::continue:: - end - - local sorted = sort_result(all_results) + local sorted = lsp_utils.sort_symbols(response) return parse_result(sorted, nil, nil) end diff --git a/lua/symbols-outline/providers/init.lua b/lua/symbols-outline/providers/init.lua index f3228e5..8f11389 100644 --- a/lua/symbols-outline/providers/init.lua +++ b/lua/symbols-outline/providers/init.lua @@ -1,7 +1,6 @@ local M = {} local providers = { - 'symbols-outline/providers/jsx', 'symbols-outline/providers/nvim-lsp', 'symbols-outline/providers/coc', 'symbols-outline/providers/markdown', diff --git a/lua/symbols-outline/providers/nvim-lsp.lua b/lua/symbols-outline/providers/nvim-lsp.lua index a067872..78d259c 100644 --- a/lua/symbols-outline/providers/nvim-lsp.lua +++ b/lua/symbols-outline/providers/nvim-lsp.lua @@ -1,4 +1,6 @@ local config = require 'symbols-outline.config' +local lsp_utils = require 'symbols-outline.utils.lsp_utils' +local jsx = require 'symbols-outline.utils.jsx' local M = {} @@ -54,13 +56,27 @@ function M.should_use_provider(bufnr) return ret end +function M.postprocess_symbols(response) + local symbols = lsp_utils.flatten_response(response) + + local jsx_symbols = jsx.get_symbols() + + if #jsx_symbols > 0 then + return lsp_utils.merge_symbols(symbols, jsx_symbols) + else + return symbols + end +end + ---@param on_symbols function function M.request_symbols(on_symbols) vim.lsp.buf_request_all( 0, 'textDocument/documentSymbol', getParams(), - on_symbols + function (response) + on_symbols(M.postprocess_symbols(response)) + end ) end diff --git a/lua/symbols-outline/providers/jsx.lua b/lua/symbols-outline/utils/jsx.lua similarity index 70% rename from lua/symbols-outline/providers/jsx.lua rename to lua/symbols-outline/utils/jsx.lua index 3944b24..a341198 100644 --- a/lua/symbols-outline/providers/jsx.lua +++ b/lua/symbols-outline/utils/jsx.lua @@ -3,34 +3,6 @@ local M = {} local SYMBOL_COMPONENT = 27 local SYMBOL_FRAGMENT = 28 -function M.should_use_provider(bufnr) - local ft = vim.api.nvim_buf_get_option(bufnr, 'ft') - local has_ts, parsers = pcall(require, 'nvim-treesitter.parsers') - local _, has_parser = pcall(function() - if has_ts then - return parsers.get_parser(bufnr) ~= nil - end - - return false - end) - - return has_ts - and has_parser - and ( - string.match(ft, 'typescriptreact') - or string.match(ft, 'javascriptreact') - ) -end - -function M.hover_info(_, _, on_info) - on_info(nil, { - contents = { - kind = 'nvim-lsp-jsx', - contents = { 'No extra information availaible!' }, - }, - }) -end - local function get_open_tag(node) if node:type() == 'jsx_element' then for _, outer in ipairs(node:field 'open_tag') do @@ -106,7 +78,7 @@ local function convert_ts(child, children, bufnr) return converted end -local function parse_ts(root, children, bufnr) +function M.parse_ts(root, children, bufnr) children = children or {} for child in root:iter_children() do @@ -118,27 +90,39 @@ local function parse_ts(root, children, bufnr) then local new_children = {} - parse_ts(child, new_children, bufnr) + M.parse_ts(child, new_children, bufnr) table.insert(children, convert_ts(child, new_children, bufnr)) else - parse_ts(child, children, bufnr) + M.parse_ts(child, children, bufnr) end end return children end -function M.request_symbols(on_symbols) - local parsers = require 'nvim-treesitter.parsers' +function M.get_symbols() + local status, parsers = pcall(require, 'nvim-treesitter.parsers') + + if not status then + return {} + end + local bufnr = 0 local parser = parsers.get_parser(bufnr) + + if parser == nil then + return {} + end + local root = parser:parse()[1]:root() - local symbols = parse_ts(root, nil, bufnr) - -- local symbols = convert_ts(ctree) - on_symbols { [1000000] = { result = symbols } } + if root == nil then + return {} + end + + return M.parse_ts(root, nil, bufnr) end return M diff --git a/lua/symbols-outline/utils/lsp_utils.lua b/lua/symbols-outline/utils/lsp_utils.lua index a0fe87c..8b5c10e 100644 --- a/lua/symbols-outline/utils/lsp_utils.lua +++ b/lua/symbols-outline/utils/lsp_utils.lua @@ -1,3 +1,6 @@ +local config = require 'symbols-outline.config' +local tbl_utils = require 'symbols-outline.utils.table' + local M = {} function M.is_buf_attached_to_lsp(bufnr) @@ -9,4 +12,174 @@ function M.is_buf_markdown(bufnr) return vim.api.nvim_buf_get_option(bufnr, 'ft') == 'markdown' end +--- Merge all client token lists in an LSP response +function M.flatten_response(response) + local all_results = {} + + -- flatten results to one giant table of symbols + for client_id, client_response in pairs(response) do + if config.is_client_blacklisted(client_id) then + print('skipping client ' .. client_id) + goto continue + end + + local result = client_response['result'] + if result == nil or type(result) ~= 'table' then + goto continue + end + + for _, value in pairs(result) do + table.insert(all_results, value) + end + + ::continue:: + end + + return all_results +end + +function M.get_selection_range(token) + -- support symbolinformation[] + -- https://microsoft.github.io/language-server-protocol/specification#textdocument_documentsymbol + if token.selectionRange == nil then + return token.location.range + end + + return token.selectionRange +end + +function M.get_range(token) + if token == nil then + return { + start={ line=math.huge, character=math.huge }, + ['end']={ line=-math.huge, character=-math.huge }, + } + end + + -- support symbolinformation[] + -- https://microsoft.github.io/language-server-protocol/specification#textdocument_documentsymbol + if token.range == nil then + return token.location.range + end + + return token.range +end + +--- lexicographically strict compare Ranges, line first +--- https://microsoft.github.io/language-server-protocol/specification/#range +local function range_compare(a, b) + if a == nil and b == nil then + return true + end + + if a == nil then + return true + end + + if b == nil then + return false + end + + return (a.line < b.line) or (a.line == b.line and a.character < b.character) +end + +--- Sorts the result from LSP by where the symbols start. +function M.sort_symbols(symbols) + table.sort(symbols, function(a, b) + return range_compare( + M.get_range(a).start, + M.get_range(b).start + ) + end) + + for _, child in ipairs(symbols) do + if child.children ~= nil then + M.sort_symbols(child.children) + end + end + + return symbols +end + +--- Preorder DFS iterator on the symbol tree +function M.symbol_preorder_iter(symbols) + local stk = {} + + local function push_stk(symbols_list) + for i = #symbols_list, 1, -1 do + table.insert(stk, symbols_list[i]) + end + end + + push_stk(symbols) + + local function is_empty() + return #stk == 0 + end + + local function next() + if not is_empty() then + local top = table.remove(stk) + + push_stk(top and top.children or {}) + + return top + end + end + + local function peek() + return stk[#stk] + end + + return { next=next, is_empty=is_empty, peek=peek } +end + +local function merge_symbols_rec(iter1, iter2, ub) + local res = {} + + while not (iter1.is_empty() and iter2.is_empty()) do + local bv1 = ((not iter1.is_empty()) and M.get_range(iter1.peek()).start) or { line=math.huge, character=math.huge } + local bv2 = ((not iter2.is_empty()) and M.get_range(iter2.peek()).start) or { line=math.huge, character=math.huge } + + local iter = (range_compare(bv1, bv2) and iter1) or iter2 + + if ub ~= nil and range_compare(ub, M.get_range(iter.peek()).start) then + break + end + + local node = iter.next() + + node.new_children = merge_symbols_rec(iter1, iter2, M.get_range(node)['end']) + + table.insert(res, node) + end + + return res +end + +--- Merge symbols from two symbol trees +--- NOTE: Symbols are mutated! +function M.merge_symbols(symbols1, symbols2) + M.sort_symbols(symbols1) + M.sort_symbols(symbols2) + + local iter1 = M.symbol_preorder_iter(symbols1) + local iter2 = M.symbol_preorder_iter(symbols2) + + local symbols = merge_symbols_rec(iter1, iter2) + + local function dfs(nodes) + for _, node in ipairs(nodes) do + dfs(node.new_children or {}) + + node.children = node.new_children + node.new_children = nil + end + end + + dfs(symbols) + + return symbols +end + return M diff --git a/lua/symbols-outline/utils/table.lua b/lua/symbols-outline/utils/table.lua index 93247d1..8c76cab 100644 --- a/lua/symbols-outline/utils/table.lua +++ b/lua/symbols-outline/utils/table.lua @@ -28,4 +28,22 @@ function M.array_copy(t) return ret end + +--- Deep copy a table, deeply excluding certain keys +function M.deepcopy_excluding(t, keys) + local res = {} + + for key, value in pairs(t) do + if not vim.tbl_contains(keys, key) then + if type(value) == "table" then + res[key] = M.deepcopy_excluding(value, keys) + else + res[key] = value + end + end + end + + return res +end + return M