From d6bd98581694c5c6ca79ccc85cebb819c4630b96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tautvydas=20S=CC=8Cidlauskas?= Date: Sat, 26 Sep 2026 21:42:36 +0300 Subject: [PATCH] feat(outline): allow disabling or overriding outline icons Adds outline.icons (true | false | per-kind table). Icons are now highlighted by position instead of syntax-matching the glyph, which also fixes METHOD and GETTER sharing one highlight entry. Closes #90 --- README.md | 3 +- lua/flutter-tools/config.lua | 6 +++- lua/flutter-tools/outline.lua | 52 +++++++++++++++++++------------ tests/outline_spec.lua | 58 +++++++++++++++++++++++++++++++++++ 4 files changed, 97 insertions(+), 22 deletions(-) create mode 100644 tests/outline_spec.lua diff --git a/README.md b/README.md index 4060507..f21bef0 100644 --- a/README.md +++ b/README.md @@ -300,7 +300,8 @@ require("flutter-tools").setup { }, outline = { open_cmd = "30vnew", -- command to use to open the outline buffer - auto_open = false -- if true this will open the outline automatically when it is first populated + auto_open = false, -- if true this will open the outline automatically when it is first populated + icons = true, -- false hides the icons (e.g. without a Nerd Font); a table overrides icons per kind, e.g. { CLASS = "C", DEFAULT = "*" } }, lsp = { on_attach = my_custom_on_attach, diff --git a/lua/flutter-tools/config.lua b/lua/flutter-tools/config.lua index b961845..fc1a289 100644 --- a/lua/flutter-tools/config.lua +++ b/lua/flutter-tools/config.lua @@ -47,7 +47,7 @@ local utils = lazy.require("flutter-tools.utils") ---@module "flutter-tools.util ---@field debugger? {enabled: boolean, exception_breakpoints?: table, evaluate_to_string_in_debug_views?: boolean, register_configurations?: fun(paths: table)} ---@field closing_tags? {highlight: string, prefix: string, priority: number, enabled: boolean} ---@field lsp? {debug?: number, color?: {enabled: boolean, background: boolean, foreground: boolean, virtual_text: boolean, virtual_text_str: string, background_color?: string}, settings?: table} ----@field outline? {auto_open: boolean, open_cmd?: string} +---@field outline? {auto_open: boolean, open_cmd?: string, icons?: boolean|table} ---@field dev_log? flutter.DevLogOpts ---@field dev_tools? {autostart: boolean, auto_open_browser: boolean} ---@field widget_preview? {web_server: boolean} @@ -75,6 +75,9 @@ local function validate_prefs(prefs) ) end vim.validate("outline", prefs.outline, "table", true) + if prefs.outline then + vim.validate("outline.icons", prefs.outline.icons, { "boolean", "table" }, true) + end vim.validate("dev_log", prefs.dev_log, "table", true) vim.validate("closing_tags", prefs.closing_tags, "table", true) end @@ -142,6 +145,7 @@ local config = { }, outline = setmetatable({ auto_open = false, + icons = true, }, { __index = function(_, k) return k == "open_cmd" and get_split_cmd(0.3, 40) or nil end, }), diff --git a/lua/flutter-tools/outline.lua b/lua/flutter-tools/outline.lua index 1feb982..860629b 100644 --- a/lua/flutter-tools/outline.lua +++ b/lua/flutter-tools/outline.lua @@ -26,7 +26,7 @@ local markers = { horizontal = "─", } -local icons = setmetatable({ +local default_icons = { TOP_LEVEL_VARIABLE = "", CLASS = "", FIELD = "󰐾", @@ -38,27 +38,38 @@ local icons = setmetatable({ ENUM = "󰉺", ENUM_CONSTANT = "", DEFAULT = "", -}, { - __index = function(t, _) return t.DEFAULT end, -}) +} local HL_PREFIX = "FlutterToolsOutline" local MARKER_HL = "FlutterToolsOutlineIndentGuides" local icon_highlights = { - [icons.TOP_LEVEL_VARIABLE] = { name = "TopLevelVar", link = "Identifier" }, - [icons.CLASS] = { name = "Class", link = "Type" }, - [icons.FIELD] = { name = "Field", link = "Identifier" }, - [icons.CONSTRUCTOR] = { name = "Constructor", link = "Identifier" }, - [icons.CONSTRUCTOR_INVOCATION] = { name = "ConstructorInvocation", link = "Special" }, - [icons.FUNCTION] = { name = "Function", link = "Function" }, - [icons.METHOD] = { name = "Method", link = "Function" }, - [icons.GETTER] = { name = "Getter", link = "Function" }, - [icons.ENUM] = { name = "Enum", link = "Type" }, - [icons.ENUM_CONSTANT] = { name = "EnumConstant", link = "Type" }, - [icons.DEFAULT] = { name = "Default", link = ui.DIM_HL }, + TOP_LEVEL_VARIABLE = { name = "TopLevelVar", link = "Identifier" }, + CLASS = { name = "Class", link = "Type" }, + FIELD = { name = "Field", link = "Identifier" }, + CONSTRUCTOR = { name = "Constructor", link = "Identifier" }, + CONSTRUCTOR_INVOCATION = { name = "ConstructorInvocation", link = "Special" }, + FUNCTION = { name = "Function", link = "Function" }, + METHOD = { name = "Method", link = "Function" }, + GETTER = { name = "Getter", link = "Function" }, + ENUM = { name = "Enum", link = "Type" }, + ENUM_CONSTANT = { name = "EnumConstant", link = "Type" }, + DEFAULT = { name = "Default", link = ui.DIM_HL }, } +---@param kind string? +---@return string? icon +---@return string? highlight +local function get_icon(kind) + local opts = config.outline.icons + if opts == false then return end + local overrides = type(opts) == "table" and opts or {} + kind = kind or "DEFAULT" + local icon = overrides[kind] or default_icons[kind] or overrides.DEFAULT or default_icons.DEFAULT + if icon == "" then return end + return icon, HL_PREFIX .. (icon_highlights[kind] or icon_highlights.DEFAULT).name +end + api.nvim_set_hl(0, MARKER_HL, { default = true, link = "NonText" }) -----------------------------------------------------------------------------// @@ -103,8 +114,8 @@ local function set_outline_highlights() for key, value in pairs(markers) do highlight_item(key:gsub("^%l", string.upper), value, MARKER_HL) end - for icon, hl in pairs(icon_highlights) do - highlight_item(hl.name, icon, hl.link) + for _, hl in pairs(icon_highlights) do + hl_link(hl.name, hl.link) end end @@ -142,10 +153,11 @@ local function parse_outline(result, node, indent, marker) local range = node.codeRange local element = node.element or {} local text = {} - local icon = icons[element.kind] - local display_str = { indent, marker, icon } - + local display_str = { indent, marker } local hl = {} + + local icon, icon_hl = get_icon(element.kind) + if icon then add_segment(display_str, hl, icon, icon_hl, #table.concat(display_str, " ")) end local length = #table.concat(display_str, " ") local return_type = element.returnType and element.returnType .. " " diff --git a/tests/outline_spec.lua b/tests/outline_spec.lua new file mode 100644 index 0000000..ec263e9 --- /dev/null +++ b/tests/outline_spec.lua @@ -0,0 +1,58 @@ +describe("outline", function() + local config + local outline + local uri = "file:///tmp/main.dart" + local class_icon = "\xef\x83\xa8" + local method_icon = "\xf3\xb0\x86\xa7" + + local function node(kind, name, children) + return { + element = { kind = kind, name = name }, + codeRange = { start = { line = 0, character = 0 }, ["end"] = { line = 1, character = 0 } }, + children = children, + } + end + + local function render(outline_config) + config.set({ outline = outline_config }) + outline.document_outline(nil, { + uri = uri, + outline = { children = { node("CLASS", "Foo", { node("METHOD", "bar") }) } }, + }) + return outline.outlines[uri] + end + + before_each(function() + package.loaded["flutter-tools.config"] = nil + package.loaded["flutter-tools.outline"] = nil + config = require("flutter-tools.config") + outline = require("flutter-tools.outline") + end) + + it("shows icons highlighted by kind by default", function() + local result = render(nil) + assert.are.same(" " .. class_icon .. " Foo", result[1].text) + assert.are.same({ + value = class_icon, + highlight = "FlutterToolsOutlineClass", + column_start = 2, + column_end = 2 + #class_icon, + }, result[1].hl[1]) + assert.are.same("FlutterToolsOutlineMethod", result[2].hl[1].highlight) + end) + + it("hides icons and their highlights when disabled", function() + local result = render({ icons = false }) + assert.are.same(" Foo", result[1].text) + assert.are.same(" └ bar", result[2].text) + assert.are.same("None", result[1].hl[1].highlight) + assert.are.same(2, result[1].hl[1].column_start) + end) + + it("uses icon overrides and falls back to the defaults", function() + local result = render({ icons = { CLASS = "C" } }) + assert.are.same(" C Foo", result[1].text) + assert.are.same(4, result[1].hl[2].column_start) + assert.are.same(" └ " .. method_icon .. " bar", result[2].text) + end) +end)