Skip to content

feat: add breadcrumb navigation bar#15573

Open
RoloEdits wants to merge 4 commits intohelix-editor:masterfrom
RoloEdits:breadcrumb
Open

feat: add breadcrumb navigation bar#15573
RoloEdits wants to merge 4 commits intohelix-editor:masterfrom
RoloEdits:breadcrumb

Conversation

@RoloEdits
Copy link
Copy Markdown
Contributor

@RoloEdits RoloEdits commented Apr 3, 2026

This PR implements a breadcrumb navigation bar at the top of the editor.

Breadcrumbs are common in other editors:

The main use for them is to help contextualize the current cursor scope, which can be hidden from all other context clues. For me personally, this alone can replace the statusline path information completely as well.

The commits should hopefully be re-viewable in order. I started by setting up the machinery in the event system to get the symbols for a document. I did need to add a new capability to tell LSPs that we support trees as a response. Without this setting, only a flat structure will be returned. I have testing this with rust-analyzer and it works as I expect. There was care to hopefully properly cache the symbols in the Document without constantly spamming the server. The symbols are kept in a tree structure for later traversal. I did make a note that perhaps this can work with flat structures too, for LSPs that don't support the nested data return, but will leave this for later. This gets the symbols from the first LSP that supports it, as i'm not sure how we would even handle multiple symbols and which gets priority that wouldn't just be the first priority anyways. One unfortunate thing here is that the DocumentSymbol is quite large(136 bytes). When storing lots of symbols, this can take up a lot of memory unnecessarily. I would love to go over ways we can optimize the size and/or the use to try to get this down.

I then set up the actual breadcrumb logic, but not yet add the UI element. This part is pretty straight forward. There are separate breadcrumbs for each view of the document, as is standard. After that, it was mainly just setting up an event that monitors the selection change, and then walks the tree to get the new symbols in the breadcrumb. Breadcrumb wraps a Vec and reuses the buffer so that allocations are kept to a minimum. One thing to be aware of is that I did not add a depth limit. This should be fine, with most nesting only being around 2 or 3, with some of the most common deep ones being 5-7, but this could hit a pathological situation, from generated code, for example. But it was simpler to implement this way. I added a configuration to enable the breadcrumb gathering. It defaults to false, meaning its disabled. There shouldn't be any extra overhead while disabled.

Finally, I added the UI element. This part is also straight forward. Just access the document and traverse the symbols for the view. Inline with other common breadcrumb implementations, I have added the relative path at the beginning. This helps the goal of adding navigational context, as there could be similar code in multiple places, so the path really is part of where the cursor is. The theming is to help the symbols pop more when looking, and take from the already defined theme keys. Its not perfect, as seen in the examples showing an impl block, but its better than just plain text. Extending this, I applied the directory theme to the path elements as well, fulfilling the visual expectation. Im also not sure if I matched up the themes to the items as they should, but this is easy to fix in followup PRs if anyone notices something strange.

The documentation still needs to be fleshed out, but I need help figuring out how the theme structure should be. Once that is done, I can implement it and add to the documentation.

AI Disclosure: I needed help finding the LSP capabilities part that would enable the hierarchical symbol configuration. I didn't know what it would be called, though in hindsight its incredibly obvious.

Much of this was taken from @matoous in #15221. Thanks for your work!

Examples:

image image image image

I did set this up in my daily driver, and added icons. This looks much better to me, but it waits on #12369 to be completed.

helix_breadcrumbs_with_icons.mp4

Related: #13492
Related: #8407
Related: #6118

@RoloEdits RoloEdits force-pushed the breadcrumb branch 2 times, most recently from 802c0ab to 3c1fff2 Compare April 3, 2026 03:22
@RoloEdits
Copy link
Copy Markdown
Contributor Author

RoloEdits commented Apr 3, 2026

Im actually not the biggest fan of the text coloring as, for example, impl blocks get returned with the impl on front, and therefore gets colored too:
image

It looks better than no colors, but pretty much every breadcrumb implementation I have seen has normal text colors, but has a colored icon. This comes across much better, but have to wait till #12369 is ready.

@RoloEdits RoloEdits force-pushed the breadcrumb branch 6 times, most recently from 37d985c to ca93344 Compare April 3, 2026 05:47
@RoloEdits RoloEdits marked this pull request as ready for review April 3, 2026 05:48
@RoloEdits
Copy link
Copy Markdown
Contributor Author

I'm not actually sure about the theme scopes here. Once I get some feedback I will add to the documentation.

@RoloEdits
Copy link
Copy Markdown
Contributor Author

Yeah, looks much better with the themed icons (at least to me):

helix_breadcrumbs_with_icons.mp4

@RoloEdits
Copy link
Copy Markdown
Contributor Author

RoloEdits commented Apr 3, 2026

I'm not sure why the tests are so slow. The tests are run with breadcrumbs = false, which should mean that there is no overhead added, but other PRs seem to still finish fine?

@RoloEdits RoloEdits mentioned this pull request Apr 3, 2026
38 tasks
@RoloEdits RoloEdits changed the title feat: add breadcrumbs feat: add breadcrumb navigation bar Apr 3, 2026
@winged winged mentioned this pull request Apr 3, 2026
@dpc
Copy link
Copy Markdown
Contributor

dpc commented Apr 4, 2026

I've been playing with it for 15 minutes, and works OK, but has a one big problem - I'm running out of screen width very quickly:

image

I'd personaly probably want to disable the path altogether, and then have an ability to shorten the very long elements of the treesitter breadcrumb .

@RoloEdits
Copy link
Copy Markdown
Contributor Author

RoloEdits commented Apr 4, 2026

@dpc I thought this would come up as well, but wasnt sure how to handle truncation.

  • Should it only show the final two symbols past some depth?
  • What depth should we stop at?
  • There is still a chance that limiting symbols is not enough, e.g., there could be only two symbols, but they are both very long. The symbol truncation alone wouldnt handle this.

What would the ideal form of that impl block look like? If we had an idea we could go from there. But keep in mind this has to work for all languages. If possible, could you see what it looks like in VSCode? Or Zed?

As for the configuration, how does this look:

[editor.breadcrumb]
enabled = true
# full: helix-term > src > commands > typed.rs > TypeableCommand > name
# file: typed.rs > TypeableCommand > name
# none: TypeableCommand > name
path = "full|file|none"

@dpc
Copy link
Copy Markdown
Contributor

dpc commented Apr 4, 2026

path = "full|file|none"

I supposed this would be enough to control the path.

As for the treesitter items maybe caping at certain length and replacing with in the middle would be enough? Rust's impl blocks got to be the worst case scenario here, and then some unreasonably long symbol names. In both cases seems like keeping beginning and end could lead to best result.

@lemonlambda
Copy link
Copy Markdown

How did you get icons?

@RoloEdits
Copy link
Copy Markdown
Contributor Author

@lemonlambda From #12369, another PR I am working on. I am using a custom branch that has other PRs and misc changes that I daily drive.

@RoloEdits
Copy link
Copy Markdown
Contributor Author

Implemented the changed configuration, but holding off on the truncation changes. Want to see if anyone else has input.

@RoloEdits
Copy link
Copy Markdown
Contributor Author

RoloEdits commented Apr 4, 2026

Hmm, I really don't know what the changes I have made here have done to cause this to hang:

// helix-term/tests/test/commands:281
async fn test_write_concurrent() -> anyhow::Result<()> {
    let mut file = tempfile::NamedTempFile::new()?;
    let mut command = String::new();
    const RANGE: RangeInclusive<i32> = 1..=1000;
    let mut app = helpers::AppBuilder::new()
        .with_file(file.path(), None)
        .build()?;

    for i in RANGE {
        let cmd = format!("%c{}<esc>:w!<ret>", i);
        command.push_str(&cmd);
    }

    test_key_sequence(&mut app, Some(&command), None, false).await?;

    reload_file(&mut file).unwrap();
    let mut file_content = String::new();
    file.read_to_string(&mut file_content)?;
    assert_eq!(
        LineFeedHandling::Native.apply(&RANGE.end().to_string()),
        file_content
    );

    Ok(())
}

By default the breadcrumb is off, so it shouldn't be hitting any paths that do a bunch of computation, and should return in the event loop right away, not wasting resources.

I checked the profile of helix actually running (not the tests) and it barely shows up, even when on:

render_breadcrumbs

(render_breadcrumb highlighted, the little sliver on the left)

Changing:

-   const RANGE: RangeInclusive<i32> = 1..=1000;
+   const RANGE: RangeInclusive<i32> = 1..=10;

And I get a (signal: 6, SIGABRT: process abort signal) error.

I'm not sure if the issues are because of my changes, or if, in some way, it only revealed an underlying issue. I know write tests have been flakey on windows, so could be?

@dpc
Copy link
Copy Markdown
Contributor

dpc commented Apr 5, 2026

test_write_concurrent

File system operations are blocking IO, and I don't see .await around these. Is this normal in helix tests? But I don't see how this would lead to a complete hang.

@RoloEdits
Copy link
Copy Markdown
Contributor Author

RoloEdits commented Apr 7, 2026

Created a new type, ThinDocumentSymbol, that only stores what we need to compute the breadcrumb from the tree taking the size from(DocumentSymbol) 136 bytes down to 56 bytes per symbol.

Doing so introduces some additional allocations, one per symbol, but we get a 2.43x memory reduction. I think this is very worth the trade off. The smaller type also should cache better, with the tree walking only missing a cache hit when it begins to walk the next symbol, but which after it starts to walk, more types should fit it cache at once.

@RoloEdits RoloEdits force-pushed the breadcrumb branch 2 times, most recently from 485d749 to 6bbf9f5 Compare April 7, 2026 22:35
@RoloEdits RoloEdits force-pushed the breadcrumb branch 3 times, most recently from b961286 to f5304e7 Compare April 7, 2026 22:59
@RoloEdits
Copy link
Copy Markdown
Contributor Author

@the-mikedavis

Something I will need help with is figuring out which event I need to change so that when you use goto definition, for example, and a new document is opened at that definition, the breadcrumb is not updated until the cursor is moved at least once. You can see this using the global search as well, when it opens a new document.

I would have thought that as this is a new document opening, this would be where you would update the breadcrumb, but it doesn't seem to be so.

@iniw
Copy link
Copy Markdown

iniw commented Apr 9, 2026

Been testing it on my fork - brilliant stuff. I've been wanting something like this for a long time!

@RoloEdits
Copy link
Copy Markdown
Contributor Author

Currently this is requesting the symbols as a user types. This is cool in that there is intimidate feedback in the breadcrumb, but this is currently a blocking operation, meaning that there is increased latency when typing. I might be able to just check if the editor is in a insert mode and just not request anymore? Or setup a debounce?

For some future work in follow-ups, I notice that zed seems to use syntax highlighting in their breadcrumbs:
image

I think for now the current coloring works, but it would be cool to look into highlighting the text itself with tree-sitter highlights, if thats what they are doing. I would have to look closer at their implementation if they are mixing LSP and tree-sitter to make theirs.

Also, as I am using > as the separator, it can blend in with other > that would be part of the symbols. Separator themeing should address this as a brute force method, but in theory the tree-sitter highlighting could as mask it as well?

image

@RoloEdits RoloEdits force-pushed the breadcrumb branch 2 times, most recently from f30307f to 49a5c18 Compare April 10, 2026 17:34
@RoloEdits
Copy link
Copy Markdown
Contributor Author

Profiled what entering a largish file and going to the bottom and creating a new function while changing the name over and over, observing that the breadcrumb was changing live:

request_document_symbols:
image

update_breadcrumbs_for_view: didnt appear in the samples

render_breadcrumb:
image

image

It doesnt look like a huge impact, but on un-optimized builds it definitely was noticeable how much input lag there was.

@gerblesh
Copy link
Copy Markdown
Contributor

gerblesh commented Apr 13, 2026

I did something similar to Zed but put the breadcrumbs path in the statusbar with my unmerged statusbar PR. For the syntax highlighting I just "reimplemented" some of the highlight query objects and matched them to the context using binary search but I'm sure there's a better way to make it more integrated using the actual highlight queries/tag queries.
image

https://codeberg.org/gwid/context.hx is the repo. I should get around to remaking this with steel components as I'm unsure as to whether or not the statusbar PR is going to be merged.

Edit: I see now that here the context is implemented with LSP so the method for highlighting is likely different as I'm unaware on how exactly the LSP protocol here works. We might be able to do some highlight query matching on top of the LSP protocol maybe(?) but not sure that would be implemented

@gerblesh
Copy link
Copy Markdown
Contributor

gerblesh commented Apr 19, 2026

Is there a fallback to use the treesitter symbols if the LSP doesn't support document symbols or if an LSP isn't available? I wasn't able to find one when looking at the changes

@RoloEdits
Copy link
Copy Markdown
Contributor Author

Yeah, this is just LSP. In a future PR we can probably support TS. Like how the symbol picker supports both.

Comment thread book/src/editor.md Outdated
@RoloEdits RoloEdits force-pushed the breadcrumb branch 2 times, most recently from 2b1c8dc to 42fda4c Compare April 19, 2026 23:05
@dpc
Copy link
Copy Markdown
Contributor

dpc commented Apr 19, 2026

This is what I dreamed of. 🥹 . I love it.

@iniw
Copy link
Copy Markdown

iniw commented Apr 22, 2026

My only feedback so far would be to make the breadcrumbs per-buffer instead of per-editor. It's current pretty disorienting to work with 2+ splits because you don't have the ability to glance at which function the unfocused splits are on. I've implemented this on my fork: iniw@7e37e77, feel free to use it as inspiration if you go on this direction.

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.

6 participants