Skip to content

Add a configuration mechanism and use it to support user customization of keymaps, user commands and region decorations.#15

Open
bpstahlman wants to merge 1 commit into
Dkendal:mainfrom
bpstahlman:feature/configurability-PR
Open

Add a configuration mechanism and use it to support user customization of keymaps, user commands and region decorations.#15
bpstahlman wants to merge 1 commit into
Dkendal:mainfrom
bpstahlman:feature/configurability-PR

Conversation

@bpstahlman
Copy link
Copy Markdown
Contributor

Overview

Adds a configuration mechanism intended to simplify installation, loading and configuration of the treeclimber plugin, and uses it to provide user customization of the following:

  • Keymaps
  • User Commands
  • Region Decoration Highlights

Also, renames 2 misleading api function names in a backwards-compatible way.
Rationale: The names select_siblings_forward and select_siblings_backward do not match their description, and thus, could be a source of confusion to new users. Aliases have been introduced to ensure the old functions can still serve as the target of mappings (though only the new names can be used as keys in the new option table).

Motivation

Currently, a user who simply wishes to override one of the default keymaps must reimplement setup_keymaps() and explicitly call setup_user_commands() and setup_augroups(). This approach exposes too much implementation detail to the user and increases the probability that a prospective user unfamiliar with the Neovim plugin system will simply give up. Moreover, although a considerable amount of work seems to have gone into the modules supporting HSLUV colors, the lack of a configuration mechanism prevents the user from benefiting from it. And because all regions other than the primary selection region use the same bg color, there is little benefit to the support of separate sibling and parent regions.

When I first started using treeclimber, I was occasionally surprised by the parent-child relationships encountered while navigating the AST: e.g., the fact that a function's parameter list is a sibling of its body felt unintuitive. Having the region decorations provide clear disambiguation between parent and child, and even between adjacent children, can reduce confusion for the new treeclimber user (or a treeclimber user working with a new language).

Installation/Loading

With a plugin manager like Lazy, a default installation/load is still as simple as adding a file like this under ~/.config/nvim/lua/plugins:

return {
  "dkendal/nvim-treeclimber",
}

Overriding the defaults is almost as easy: e.g., to customize the keymaps for select_forward/backward and add bold-italic to the decoration for the currently-selected node...

return {
  "dkendal/nvim-treeclimber",
  opts = {
    ui = {
      keys = {
        select_forward = "<A-w>",
        select_backward = "<A-b>",
      },
    },
    display = {
      regions = {
        highlights = {
          Selection = {bold = true, italic = true},
        },
      },
    },
  }
}

Existing users should not be affected by these changes, as it's still possible to call the setup() function explicitly, with or without an option table. As before, omitting the option table in the call to setup() requests defaults.

Option Table Support

Module opt provides a generic interface to a hierarchical option table, which encapsulates a table of defaults and (optionally) a table of user overrides. The treeclimber defaults are defined in the singleton module config, which creates and manages an opt instance capable of serving requests for specific option keys. The option table is organized in sub-trees: e.g., ui for keymaps and user commands, display for highlights, etc. The default option table is documented with comments in the README.md. The config module provides get() and get_default() methods, which accept option keys as a string of dot-separated components: e.g., ui.keys and display.regions.highlights. The opt module does more than simply merge user overrides into the default table. The reason is that users occasionally provide malformed option tables, and when they do, plugins based on the idiomatic vim.api.tbl_deep_extend() approach tend to spew errors and die, leaving the plugin in an indeterminate state. The approach in this PR, built around the opt module, is to fall back to sane defaults with a warning.

Keymap Customization

The option table's nested key ui.keys may be used to customize (without completely replacing) the default keymaps.
This table contains a separate key for each treeclimber api function. The key value format is flexible enough to acommodate the most complex scenarios, while keeping the simple (and common) use case simple: e.g., to change the default keybinding for select_forward to <A-w>...

  select_forward = "<A-w>",

To create different maps in visual/select and normal/operator-pending modes...

  select_forward = { {"x", ">"}, {{"n", "o"}, "<A-w>"} },

To disable a map altogether...

  select_forward = false,

User Command Customization

The option table's nested key ui.cmds may be used to customize the default user commands, changing the command name or disabling the command altogether.

Region Highlighting Customization

The current implementation suffers from the following limitations pertaining to region decoration:

  • All regions other than the current selection use the same bg color, making it impossible to distinguish between parent and sibling groups (and there's no way for the user to change this).
  • A bug in apply_decoration causes all adjacent siblings to combine into a single visual group; thus, even if the user manually altered the bg colors, the desired differentiation between groups would not be achieved.
  • Highlight attributes such as bold and italic are not supported. When used properly, such attributes may be used to make regions stand out significantly better than do bg colors alone.

The display.regions.highlights key allows the user to tailor the highlighting of the various groups and/or disable unwanted groups altogether. The format of the key is flexible and fully documented in the README. The simplest key values are vim.api.keyset.highlight objects containing fg/bg colors and/or attributes like 'bold' and 'italic'. But they can also be callback functions that return such objects. Callbacks permit use of HSLUVHighlight objects that may not be available until colorscheme load, which is most likely after the user option table has been defined.

There's also an option that determines whether attributes defined for the parent groups "bleed through" to the children (siblings). Because of the way Neovim handls extmark hl_groups, attributes set on the Parent also appear in the contained Siblings. If bleed-through is not desired, it's necessary to break the Parent into discontiguous regions. (An optimization would permit use of a single Parent if it's determined that none of the hl_groups use such attributes, but the optimization has not been implemented.) There's also an option that determines whether a user-provided highlight combines with the parent or replaces it. This affects only attributes like bold and italic, since Neovim doesn't combine the bg colors.

The default parent and sibling bg colors are now visuallly distinct, and the reworked apply_decoration() intentionally allows the parent highlight to show through in the space between named children, making it easier to tell exactly where one sibling ends and another begins.

The setup() function now takes a user option table.
A new `opt` module manages both the user option table and a corresponding
default option table, and serves requests for hierarchical option names
(represented as dot-separated strings).
The `opt` module's query function supports fallback to default and
"repair" of incorrectly formatted user option tables.

The new option mechanism is used to support user customization of the following:
* keymaps
* user commands
* region decorations

The region decoration mechanism has been enhanced to permit better
differentiation between the various types of region.

Modified get_larger_ancestor() to make it return the outermost of several
colocated nodes.
Rationale: If innermost node is returned, parent node decoration will not be
visible when there are colocated nodes.
@Dkendal
Copy link
Copy Markdown
Owner

Dkendal commented May 2, 2025

Hey thanks for this, and clearly the high level of effort! I'll review it this weekend.

@bpstahlman
Copy link
Copy Markdown
Contributor Author

Hey thanks for this, and clearly the high level of effort! I'll review it this weekend.

My pleasure! Sounds good.

@bpstahlman
Copy link
Copy Markdown
Contributor Author

@Dkendal Did you ever get a chance to review?
If so, let me know if you found issues or anything that would need to be changed before a merge...

@bpstahlman
Copy link
Copy Markdown
Contributor Author

@Dkendal Did you ever get a chance to review?
If so, let me know if you found issues or anything that would need to be changed before a merge...

@Dkendal I completely understand if you just don't have time to review the PR right now, but would appreciate it if you'd confirm receipt of my previous comment…

Copy link
Copy Markdown
Owner

@Dkendal Dkendal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry again for the delay, life gets busy!

There are quite a few good ideas in this, but in it's current form I don't want to merge.

In general, I'd like to move this plugin more in line with the advice listed here. Specifically this means:

  • removing any configuration specific DSL,
  • moving configuration to be as lazy as possible (no setup call)
  • not providing default mappings, only <plug> mappings

This PR contains a number of configuration specific DSLs that move away from this goal, and specifically for the keymap example I don't see a benefit over <plug> keymaps, and there is quite a lot of new configuration specific code that I'm reticent to maintain.

If it's okay with you I'd like to close this PR and cherry pick some of the ideas?

Comment thread README.md
Comment on lines +183 to +190
highlights = {
Selection = function(o) return { bold = true, bg = o.visual.bg.hex } end,
SiblingStart = false,
Sibling = function(o) return { bg = o.visual.bg.mix(o.normal.bg, 50).hex } end,
-- Make Parent bg color noticeably lighter than Siblings.
Parent = function(o) return { bg = o.visual.bg.mix(o.normal.bg, 80).hex } end,
ParentStart = false,
},
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of having these highlights configurable - however the choice of the current normal and visual highlight feels arbitrary. I would prefer either make the callback accept 0 parameters, or have one parameter which is the hsluv module.

I'm somewhat of the mind that it's more flexible to not include this in configuration, beyond static values - and just provide an example in the readme on how to define an autocommand that sets the highlights when the colourscheme changes.

Comment thread README.md
Comment on lines +191 to +195
-- `true` to cause attributes like bold and italic to "bleed through" from Parent
-- to Siblings.
inherit_attrs = true,
-- `true` to replace default 'highlights' with user overrides, false or nil to merge
replace_defaults = false,
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to the comments above, I would prefer to not introduce any custom configuration DSL, as it increases the documentation and learning surface area. I'm okay with exposing options that are just delegates to vim api functions, but for anything more advanced I'd prefer to only provide example in the README.

This is inline with the advice here.

Comment thread lua/nvim-treeclimber.lua
Comment on lines +84 to +85
function M.setup_keymaps()
---@type table<string, treeclimber.KeymapEntry>|boolean
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have a substantive argument here, but I would really prefer not to support a custom DSL for define keymaps.

The direction I'd like this plugin to move is to use <plug> keymaps which is inline with this advice. What benefit does adding this custom DSL provide?

@bpstahlman
Copy link
Copy Markdown
Contributor Author

@Dkendal I appreciate your taking the time to provide a detailed review. I've read over your comments, but it will be a day or two before I can take the time to go through the linked documentation and provide feedback...

@Dkendal
Copy link
Copy Markdown
Owner

Dkendal commented Sep 8, 2025 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants