diff --git a/lib/helpers.nix b/lib/helpers.nix index b5ba404b..3e134aa1 100644 --- a/lib/helpers.nix +++ b/lib/helpers.nix @@ -161,6 +161,7 @@ with lib; rec { '' ); + mkNum = default: mkNullable lib.types.number (toString default); mkInt = default: mkNullable lib.types.int (toString default); mkBool = default: mkNullable lib.types.bool ( diff --git a/plugins/default.nix b/plugins/default.nix index deff4a8e..aa475b13 100644 --- a/plugins/default.nix +++ b/plugins/default.nix @@ -55,6 +55,8 @@ ./telescope + ./ui/noice.nix + ./utils/comment-nvim.nix ./utils/commentary.nix ./utils/easyescape.nix diff --git a/plugins/ui/noice.nix b/plugins/ui/noice.nix new file mode 100644 index 00000000..36c7376c --- /dev/null +++ b/plugins/ui/noice.nix @@ -0,0 +1,353 @@ +{ + lib, + pkgs, + config, + ... +} @ args: +# TODO: This uses a lot of types.anything because noice.nvim types are quite complex. +# It should be possible to map them to nix, but they would not map really well through +# toLuaObject, we would maybe need some ad-hoc pre-processing functions. +with lib; let + helpers = import ../helpers.nix args; +in { + options.plugins.noice = + helpers.extraOptionsOptions + // { + enable = mkEnableOption '' + noice.nvim, an experimental nvim UI. + Note that if treesitter is enabled you need the following parsers: + vim, regex, lua, bash, markdown, markdown_inline + ''; + + package = helpers.mkPackageOption "noice" pkgs.vimPlugins.noice-nvim; + + cmdline = helpers.mkCompositeOption "" { + enabled = helpers.defaultNullOpts.mkBool true "enables Noice cmdline UI"; + view = helpers.defaultNullOpts.mkStr "cmdline_popup" ""; + opts = helpers.defaultNullOpts.mkNullable types.anything "{}" ""; + format = + helpers.defaultNullOpts.mkNullable (types.attrsOf types.anything) '' + { + cmdline = {pattern = "^:"; icon = ""; lang = "vim";}; + search_down = {kind = "search"; pattern = "^/"; icon = " "; lang = "regex";}; + search_up = {kind = "search"; pattern = "?%?"; icon = " "; lang = "regex";}; + filter = {pattern = "^:%s*!"; icon = "$"; lang = "bash";}; + lua = {pattern = "^:%s*lua%s+"; icon = ""; lang = "lua";}; + help = {pattern = "^:%s*he?l?p?%s+"; icon = "";}; + input = {}; + } + '' '' + conceal: (default=true) This will hide the text in the cmdline that matches the pattern. + view: (default is cmdline view) + opts: any options passed to the view + icon_hl_group: optional hl_group for the icon + title: set to anything or empty string to hide + lua = false, to disable a format, set to `false` + ''; + }; + + messages = + helpers.mkCompositeOption + "NOTE: If you enable messages, then the cmdline is enabled automatically" { + enabled = helpers.defaultNullOpts.mkBool true "enables the messages UI"; + view = helpers.defaultNullOpts.mkStr "notify" "default view for messages"; + viewError = helpers.defaultNullOpts.mkStr "notify" "default view for errors"; + viewWarn = helpers.defaultNullOpts.mkStr "notify" "default view for warnings"; + viewHistory = helpers.defaultNullOpts.mkStr "messages" "view for :messages"; + viewSearch = helpers.defaultNullOpts.mkStr "virtualtext" "view for search count messages"; + }; + + popupmenu = helpers.mkCompositeOption "" { + enabled = helpers.defaultNullOpts.mkBool true "enables the Noice popupmenu UI"; + backend = helpers.defaultNullOpts.mkEnumFirstDefault ["nui" "cmp"] ""; + kindIcons = + helpers.defaultNullOpts.mkNullable + (types.either types.bool (types.attrsOf types.anything)) "{}" + "Icons for completion item kinds. set to `false` to disable icons"; + }; + + redirect = helpers.defaultNullOpts.mkNullable (types.attrsOf types.anything) '' + { + view = "popup"; + filter = {event = "msg_show";}; + } + '' "default options for require('noice').redirect"; + + commands = helpers.defaultNullOpts.mkNullable (types.attrsOf types.anything) '' + { + history = { + view = "split"; + opts = {enter = true; format = "details";}; + filter = { + any = [ + {event = "notify";} + {error = true;} + {warning = true;} + {event = "msg_show"; kind = [""];} + {event = "lsp"; kind = "message";} + ]; + }; + }; + last = { + view = "popup"; + opts = {enter = true; format = "details";}; + filter = { + any = [ + {event = "notify";} + {error = true;} + {warning = true;} + {event = "msg_show"; kind = [""];} + {event = "lsp"; kind = "message";} + ]; + }; + filter_opts = {count = 1;}; + }; + errors = { + view = "popup"; + opts = {enter = true; format = "details";}; + filter = {error = true;}; + filter_opts = {reverse = true;}; + }; + } + '' "You can add any custom commands that will be available with `:Noice command`"; + + notify = + helpers.mkCompositeOption '' + Noice can be used as `vim.notify` so you can route any notification like other messages + Notification messages have their level and other properties set. + event is always "notify" and kind can be any log level as a string + The default routes will forward notifications to nvim-notify + Benefit of using Noice for this is the routing and consistent history view + '' { + enabled = helpers.defaultNullOpts.mkBool true "enable notification handling"; + view = helpers.defaultNullOpts.mkStr "notify" ""; + }; + + lsp = helpers.mkCompositeOption "" { + progress = helpers.mkCompositeOption "" { + enabled = helpers.defaultNullOpts.mkBool true "enable LSP progress"; + + format = + helpers.defaultNullOpts.mkNullable + (types.either types.str types.anything) ''"lsp_progress"'' '' + Lsp Progress is formatted using the builtins for lsp_progress + ''; + formatDone = + helpers.defaultNullOpts.mkNullable + (types.either types.str types.anything) ''"lsp_progress"'' ""; + + throttle = + helpers.defaultNullOpts.mkNum "1000 / 30" + "frequency to update lsp progress message"; + + view = helpers.defaultNullOpts.mkStr "mini" ""; + }; + + override = helpers.defaultNullOpts.mkNullable (types.attrsOf types.bool) '' + { + "vim.lsp.util.convert_input_to_markdown_lines" = false; + "vim.lsp.util.stylize_markdown" = false; + "cmp.entry.get_documentation" = false; + } + '' ""; + + hover = helpers.mkCompositeOption "" { + enabled = helpers.defaultNullOpts.mkBool true "enable hover UI"; + view = + helpers.defaultNullOpts.mkNullable types.str "null" + "when null, use defaults from documentation"; + opts = + helpers.defaultNullOpts.mkNullable types.anything "{}" + "merged with defaults from documentation"; + }; + + signature = helpers.mkCompositeOption "" { + enabled = helpers.defaultNullOpts.mkBool true "enable signature UI"; + + autoOpen = helpers.mkCompositeOption "" { + enabled = helpers.defaultNullOpts.mkBool true ""; + trigger = + helpers.defaultNullOpts.mkBool true + "Automatically show signature help when typing a trigger character from the LSP"; + luasnip = + helpers.defaultNullOpts.mkBool true + "Will open signature help when jumping to Luasnip insert nodes"; + throttle = helpers.defaultNullOpts.mkNum 50 '' + Debounce lsp signature help request by 50ms + ''; + }; + + view = + helpers.defaultNullOpts.mkNullable types.str "null" + "when null, use defaults from documentation"; + opts = + helpers.defaultNullOpts.mkNullable types.anything "{}" + "merged with defaults from documentation"; + }; + + message = helpers.mkCompositeOption "Messages shown by lsp servers" { + enabled = helpers.defaultNullOpts.mkBool true "enable display of messages"; + + view = helpers.defaultNullOpts.mkStr "notify" ""; + opts = helpers.defaultNullOpts.mkNullable types.anything "{}" ""; + }; + + documentation = helpers.mkCompositeOption "defaults for hover and signature help" { + view = helpers.defaultNullOpts.mkStr "hover" ""; + + opts = helpers.defaultNullOpts.mkNullable types.anything '' + { + lang = "markdown"; + replace = true; + render = "plain"; + format = ["{message}"]; + win_options = { concealcursor = "n"; conceallevel = 3; }; + } + '' ""; + }; + }; + + markdown = helpers.mkCompositeOption "" { + hover = helpers.defaultNullOpts.mkNullable (types.attrsOf types.str) '' + { + "|(%S-)|" = helpers.mkRaw "vim.cmd.help"; // vim help links + "%[.-%]%((%S-)%)" = helpers.mkRaw "require("noice.util").open"; // markdown links + } + '' "set handlers for hover (lua code)"; + + highlights = helpers.defaultNullOpts.mkNullable (types.attrsOf types.str) '' + { + "|%S-|" = "@text.reference"; + "@%S+" = "@parameter"; + "^%s*(Parameters:)" = "@text.title"; + "^%s*(Return:)" = "@text.title"; + "^%s*(See also:)" = "@text.title"; + "{%S-}" = "@parameter"; + } + '' "set highlight groups"; + }; + + health = helpers.mkCompositeOption "" { + checker = + helpers.defaultNullOpts.mkBool true + "Disable if you don't want health checks to run"; + }; + + smartMove = + helpers.mkCompositeOption + "noice tries to move out of the way of existing floating windows." { + enabled = helpers.defaultNullOpts.mkBool true "you can disable this behaviour here"; + excludedFiletypes = + helpers.defaultNullOpts.mkNullable (types.listOf types.str) + ''[ "cmp_menu" "cmp_docs" "notify"]'' '' + add any filetypes here, that shouldn't trigger smart move + ''; + }; + + presets = + helpers.defaultNullOpts.mkNullable (types.either types.bool types.anything) '' + { + bottom_search = false; + command_palette = false; + long_message_to_split = false; + inc_rename = false; + lsp_doc_border = false; + } + '' " + you can enable a preset by setting it to true, or a table that will override the preset + config. you can also add custom presets that you can enable/disable with enabled=true + "; + + throttle = helpers.defaultNullOpts.mkNum "1000 / 30" '' + how frequently does Noice need to check for ui updates? This has no effect when in blocking + mode + ''; + + views = helpers.defaultNullOpts.mkNullable (types.attrsOf types.anything) "{}" ""; + routes = helpers.defaultNullOpts.mkNullable (types.attrsOf types.anything) "{}" ""; + status = helpers.defaultNullOpts.mkNullable (types.attrsOf types.anything) "{}" ""; + format = helpers.defaultNullOpts.mkNullable (types.attrsOf types.anything) "{}" ""; + }; + + config = let + cfg = config.plugins.noice; + setupOptions = { + inherit (cfg) presets routes status format; + cmdline = helpers.ifNonNull' cfg.cmdline { + inherit (cfg.cmdline) enabled view opts format; + }; + messages = let + cfgM = cfg.messages; + in + helpers.ifNonNull' cfgM { + inherit (cfgM) enabled view; + view_error = cfgM.viewError; + view_warn = cfgM.viewWarn; + view_history = cfgM.viewHistory; + view_search = cfgM.viewSearch; + }; + popupmenu = let + cfgP = cfg.popupmenu; + in + helpers.ifNonNull' cfgP { + inherit (cfgP) enabled backend; + kind_icons = cfgP.kindIcons; + }; + inherit (cfg) redirect commands; + notify = helpers.ifNonNull' cfg.notify { + inherit (cfg.notify) enabled view; + }; + lsp = let + cfgL = cfg.lsp; + in + helpers.ifNonNull' cfgL { + progress = let + cfgLP = cfgL.progress; + in + helpers.ifNonNull' cfgLP { + inherit (cfgLP) enabled format throttle view; + format_done = cfgLP.formatDone; + }; + override = cfgL.override; + hover = helpers.ifNonNull' cfgL.hover { + inherit (cfgL.hover) enabled view opts; + }; + sigature = let + cfgLS = cfgL.signature; + in + helpers.ifNonNull' cfgLS { + inherit (cfgLS) enabled view opts; + auto_open = helpers.ifNonNull' cfgLS.autoOpen { + inherit (cfgLS.autoOpen) enabled trigger luasnip throttle; + }; + }; + message = helpers.ifNonNull' cfgL.message { + inherit (cfgL.message) enabled view opts; + }; + documentation = helpers.ifNonNull' cfgL.documentation { + inherit (cfgL.documentation) view opts; + }; + }; + markdown = helpers.ifNonNull' cfg.markdown { + inherit (cfg.markdown) hover highlights; + }; + health = helpers.ifNonNull' cfg.health { + inherit (cfg.health) checker; + }; + smart_move = let + cfgS = cfg.smartMove; + in + helpers.ifNonNull' cfgS { + inherit (cfgS) enabled; + excluded_filetypes = cfgS.excludedFiletypes; + }; + }; + in + mkIf cfg.enable { + # nui-nvim & nvim-notify are dependencies of the vimPlugins.noice-nvim package + extraPlugins = [cfg.package]; + extraConfigLua = '' + require("noice").setup(${helpers.toLuaObject setupOptions}) + ''; + }; +} diff --git a/tests/plugins/noice.nix b/tests/plugins/noice.nix new file mode 100644 index 00000000..ff4f8f5f --- /dev/null +++ b/tests/plugins/noice.nix @@ -0,0 +1,208 @@ +{ + empty = { + plugins.noice.enable = true; + }; + + defaults = { + plugins.noice = { + enable = true; + cmdline = { + enabled = true; + view = "cmdline_popup"; + opts = {}; + format = { + cmdline = { + pattern = "^:"; + icon = ""; + lang = "vim"; + }; + search_down = { + kind = "search"; + pattern = "^/"; + icon = " "; + lang = "regex"; + }; + search_up = { + kind = "search"; + pattern = "^%?"; + icon = " "; + lang = "regex"; + }; + filter = { + pattern = "^:%s*!"; + icon = "$"; + lang = "bash"; + }; + lua = { + pattern = "^:%s*lua%s+"; + icon = ""; + lang = "lua"; + }; + help = { + pattern = "^:%s*he?l?p?%s+"; + icon = ""; + }; + input = {}; + }; + }; + messages = { + enabled = true; + view = "notify"; + viewError = "notify"; + viewWarn = "notify"; + viewHistory = "messages"; + viewSearch = "virtualtext"; + }; + popupmenu = { + enabled = true; + backend = "nui"; + kindIcons = {}; + }; + redirect = { + view = "popup"; + filter = {event = "msg_show";}; + }; + commands = { + history = { + view = "split"; + opts = { + enter = true; + format = "details"; + }; + filter = { + any = [ + {event = "notify";} + {error = true;} + {warning = true;} + { + event = "msg_show"; + kind = [""]; + } + { + event = "lsp"; + kind = "message"; + } + ]; + }; + }; + last = { + view = "popup"; + opts = { + enter = true; + format = "details"; + }; + filter = { + any = [ + {event = "notify";} + {error = true;} + {warning = true;} + { + event = "msg_show"; + kind = [""]; + } + { + event = "lsp"; + kind = "message"; + } + ]; + }; + filter_opts = {count = 1;}; + }; + errors = { + view = "popup"; + opts = { + enter = true; + format = "details"; + }; + filter = {error = true;}; + filter_opts = {reverse = true;}; + }; + }; + notify = { + enabled = true; + view = "notify"; + }; + lsp = { + progress = { + enabled = true; + format = "lsp_progress"; + formatDone = "lsp_progress_done"; + throttle = 1000 / 30; + view = "mini"; + }; + override = { + "vim.lsp.util.convert_input_to_markdown_lines" = false; + "vim.lsp.util.stylize_markdown" = false; + "cmp.entry.get_documentation" = false; + }; + hover = { + enabled = true; + view = null; + opts = {}; + }; + signature = { + enabled = true; + autoOpen = { + enabled = true; + trigger = true; + luasnip = true; + throttle = 50; + }; + view = null; + opts = {}; + }; + message = { + enabled = true; + view = "notify"; + opts = {}; + }; + documentation = { + view = "hover"; + opts = { + lang = "markdown"; + replace = true; + render = "plain"; + format = ["{message}"]; + win_options = { + concealcursor = "n"; + conceallevel = 3; + }; + }; + }; + }; + markdown = { + hover = { + "|(%S-)|" = "vim.cmd.help"; + "%[.-%]%((%S-)%)" = ''require("noice.util").open''; + }; + highlights = { + "|%S-|" = "@text.reference"; + "@%S+" = "@parameter"; + "^%s*(Parameters:)" = "@text.title"; + "^%s*(Return:)" = "@text.title"; + "^%s*(See also:)" = "@text.title"; + "{%S-}" = "@parameter"; + }; + }; + health = { + checker = true; + }; + smartMove = { + enabled = true; + excludedFiletypes = ["cmp_menu" "cmp_docs" "notify"]; + }; + presets = { + bottom_search = false; + command_palette = false; + long_message_to_split = false; + inc_rename = false; + lsp_doc_border = false; + }; + throttle = 1000 / 30; + views = {}; + routes = {}; + status = {}; + format = {}; + }; + }; +}