Skip to content

[fix] fix tab insertion#6196

Merged
nathanlesage merged 14 commits into
Zettlr:developfrom
benniekiss:fix-tabs
Mar 1, 2026
Merged

[fix] fix tab insertion#6196
nathanlesage merged 14 commits into
Zettlr:developfrom
benniekiss:fix-tabs

Conversation

@benniekiss

Copy link
Copy Markdown
Collaborator

Description

This PR fixes tab insertion and improves YAML tab-to-space sanitation.

Closes #6190

Changes

The codemirror insertTab command was reimplemented. The native command always inserts tabs (when not calling indentMore), so we need a custom implementation that inserts spaces or tabs. The functionality was separated from the list helper function. As a result, a new keybinding was added.

The YAMLFrontmatter sanitation was improved. The isInYamlFrontmatter function was moved to its own file and renamed to yamlInSelection, mirroring the rangeInSelection utility. In addition, if a selection is within a yaml node, the indentWithTabs config setting is overriden temporarily. This is necessary so that indentMore uses spaces when within YAMLFrontmatter nodes. Otherwise, we still get tab inserts.

Finally, the isListTouchedBySelection was refactored and renamed to listInRange to mirror the other similar functions. It was refactored to use tree.resolve() rather than iterate over the tree.

Additional information

Tested on:

macOS 26

@benniekiss benniekiss marked this pull request as ready for review February 19, 2026 03:23
@benniekiss

Copy link
Copy Markdown
Collaborator Author

The latest commit further changes yamlInSelection into a more generic function for checking if a list of nodes is within a selection. It is now called nodeInSelection, and an additional helper was added posInSelection, both of which consolidate logic used in a few places in the code

@nathanlesage

Copy link
Copy Markdown
Member

I appreciate this, especially ensuring that YAML frontmatters won't contain tab characters anymore.

However, I think we should spend at least a little bit on discussing the implementation, because — at least for me, personally — this PR would prove to be an unwanted change. To elaborate a bit:

Tab characters stem from a time when we didn't have tables and proper alignment, so they have lost their original meaning. Nowadays, they mostly are used to switch between program tabs, or to indent/unindent lists, but even this indentation is at least half of the time done with spaces. So, oftentimes, they are at the very least not necessary and thus a matter of preference.

If I understand you correctly, this would change the behavior so that, when one is somewhere within a line, this would no longer indent the line, but instead insert a literal tab character. This would be a regression, because I have grown accustomed to just clicking somewhere in a line to indent/unindent it.

However, I fully understand and support workflows that rely on tabs instead of spaces, so I am more than happy to implement this, but with some changes.

Thus, I have two refactoring requests:

  1. Please separate the switching-off of tabs in the frontmatter from the indentation logic, if possible. (This might necessitate two separate commands both bound to Tab with a fall-through like behavior, I apologize for this in advance. But maybe this could be done within a single command…?)
  2. Allow toggling of this behavior, with the default being the current behavior (i.e., always indent/unindent, never inserting a space). You can just amend the existing config objects and add this setting to the other section of the editor settings (unless you find a better place).

Thank you!

@benniekiss

benniekiss commented Feb 19, 2026

Copy link
Copy Markdown
Collaborator Author

Sure, I can make it configurable. I think it should default to allowing inserting tabs/spaces in the text, since it would otherwise be a big change from previous Zettlr behavior, which currently allows inserting tabs.

I don't think I should separate out the YAML logic, though. The YAML case would be an exact duplicate, except for the YAML check.

If I understand you correctly, this would change the behavior so that, when one is somewhere within a line, this would no longer indent the line, but instead insert a literal tab character. This would be a regression, because I have grown accustomed to just clicking somewhere in a line to indent/unindent it.

Just to clarify, this would only be a regression from the changes in #6168. Before that PR, Zettlr used the codemirror insertTab, which either inserts a literal tab, or indents the line if the selection is non-empty.

@benniekiss

Copy link
Copy Markdown
Collaborator Author

Another point -- there are three places in the code that could re-use the new posInNode function. I will save those refactors for a separate review, unless you think I should include them here.

  • /**
    * Given the editor state and a position, this function returns whether the
    * position sits within a node that is protected from autocorrect. In those
    * cases, no autocorrection will be applied, regardless of whether there is a
    * suitable candidate.
    *
    * @param {EditorState} state The state
    * @param {number} pos The position to check
    *
    * @return {boolean} True if the position touches a protected node.
    */
    function posInProtectedNode (state: EditorState, pos: number): boolean {
    const PROTECTED_NODES = [
    'InlineCode', // `code`
    'Comment', 'CommentBlock', // <!-- comment -->
    'FencedCode', 'CodeText', // Code block
    'HorizontalRule', // --- and ***
    'YAMLFrontmatter',
    'HTMLTag', 'HTMLBlock' // HTML elements
    ]
    let node = syntaxTree(state).resolveInner(pos, -1)
    while (node.parent !== null) {
    if (PROTECTED_NODES.includes(node.type.name)) {
    return true
    }
    node = node.parent
    }
    // Neither the node itself, nor any of its parents, are protected.
    return false
    }

  • /**
    * Takes an EditorView and a position within it, and returns either a SyntaxNode
    * of type URL, Link, or Image, or null. This information can be used to
    * determine whether there is any form of Link or image at the given position.
    *
    * @param {EditorView} view The editor view
    * @param {number} pos The position to check
    *
    * @return {SyntaxNode|null} Either a Link, Image, or URL syntax node, or null.
    */
    function getLinkOrImageNodeFromPos (view: EditorView, pos: number): SyntaxNode|null {
    const node = syntaxTree(view.state).resolveInner(pos)
    if ([ 'URL', 'Link', 'Image' ].includes(node.type.name)) {
    return node
    }
    let nodeAt: SyntaxNode|null = node
    while (nodeAt !== null && ![ 'Link', 'Image', 'LinkReference' ].includes(nodeAt.type.name)) {
    nodeAt = nodeAt.parent
    }
    return nodeAt
    }

  • /**
    * Displays a tooltip for URLs and Links across a document
    */
    export function urlTooltip (view: EditorView, pos: number, side: 1 | -1): Tooltip|null {
    let nodeAt = syntaxTree(view.state).cursorAt(pos, side).node
    // If the node here is a URL, it's quick, but if not, it must be a Link node
    // that contains a URL as a child
    if (nodeAt.type.name !== 'URL') {
    // First, navigate to one of the two possible parent nodes.
    while (nodeAt.parent !== null && ![ 'Link', 'LinkReference' ].includes(nodeAt.type.name)) {
    nodeAt = nodeAt.parent
    }

@nathanlesage

Copy link
Copy Markdown
Member

Sure, I can make it configurable. I think it should default to allowing inserting tabs/spaces in the text, since it would otherwise be a big change from previous Zettlr behavior, which currently allows inserting tabs.

I don't think I should separate out the YAML logic, though. The YAML case would be an exact duplicate, except for the YAML check.

If I understand you correctly, this would change the behavior so that, when one is somewhere within a line, this would no longer indent the line, but instead insert a literal tab character. This would be a regression, because I have grown accustomed to just clicking somewhere in a line to indent/unindent it.

Just to clarify, this would only be a regression from the changes in #6168. Before that PR, Zettlr used the codemirror insertTab, which either inserts a literal tab, or indents the line if the selection is non-empty.

Oh god, I completely overlooked that, which might be due to me never wanting Tabs in my files, so I didn't realize this. So yes, your change is correct. Let me go through this again, because given the fact that it worked the last years without me complaining means it worked as intended. So no need to make this configurable.

@nathanlesage

Copy link
Copy Markdown
Member

Ah I see you made it configurable already. Then let's keep that, it can't hurt, and this way your effort wasn't wasted!

@nathanlesage nathanlesage left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks good, the only thing I wanted to change is the config variable to be more verbose; otherwise it may be difficult to see what it does. Other than that, this thing is ready to merge. You should be able to just go through the suggestions and apply them in order, I think!

Move any additional refactors to a new PR! Thanks!

Comment thread source/common/modules/markdown-editor/commands/lists.ts
Comment thread source/app/service-providers/config/get-config-template.ts Outdated
Comment thread source/app/service-providers/config/get-config-template.ts Outdated
Comment thread source/common/modules/markdown-editor/util/configuration.ts Outdated
Comment thread source/common/modules/markdown-editor/util/configuration.ts Outdated
Comment thread source/win-preferences/schema/editor.ts Outdated
Comment thread source/win-main/MainEditor.vue Outdated
Comment thread source/common/modules/markdown-editor/commands/markdown.ts Outdated
Comment thread source/common/modules/markdown-editor/commands/markdown.ts Outdated
},
{
type: 'checkbox',
label: trans('Always indent the current line when pressing Tab'),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
label: trans('Always indent the current line when pressing Tab'),
label: trans('Always indent the current line when pressing Tab'),
info: trans('Zettlr always indents list items. Turn this setting on to always indent any line and never insert Tab characters.'),

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

So this description is not entirely accurate, as it doesn't control whether tab characters are inserted or not, that would be controlled by the setting above, indentWithTab. It only determines whether pressing tab always indents the line, or if it inserts an indent unit at the cursor.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Hmhm, I'll think about this and improve the wording later on. No need to worry about this detail at this point :)

benniekiss and others added 9 commits February 26, 2026 17:23
Co-authored-by: Hendrik Erz <hendrik@zettlr.com>
Co-authored-by: Hendrik Erz <hendrik@zettlr.com>
Co-authored-by: Hendrik Erz <hendrik@zettlr.com>
Co-authored-by: Hendrik Erz <hendrik@zettlr.com>
Co-authored-by: Hendrik Erz <hendrik@zettlr.com>
Co-authored-by: Hendrik Erz <hendrik@zettlr.com>
Co-authored-by: Hendrik Erz <hendrik@zettlr.com>
Co-authored-by: Hendrik Erz <hendrik@zettlr.com>
@nathanlesage

Copy link
Copy Markdown
Member

Thank you!

@nathanlesage nathanlesage merged commit fe7b8f7 into Zettlr:develop Mar 1, 2026
1 check passed
@benniekiss benniekiss deleted the fix-tabs branch March 1, 2026 15:04
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.

Pressing tab within a line indents the whole line

2 participants