Skip to content

Commit 7d43d39

Browse files
committed
feat: Show filepath context in bufferline
This change adds functionality that computes the shortest unique filepath for each document in the editor to display as the title in the bufferline, along with the appropriate configuration options.
1 parent ebf96bd commit 7d43d39

File tree

3 files changed

+207
-19
lines changed

3 files changed

+207
-19
lines changed

book/src/editor.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
- [`[editor]` Section](#editor-section)
44
- [`[editor.clipboard-provider]` Section](#editorclipboard-provider-section)
55
- [`[editor.statusline]` Section](#editorstatusline-section)
6+
- [`[editor.bufferline]` Section](#editorbufferline-section)
67
- [`[editor.lsp]` Section](#editorlsp-section)
78
- [`[editor.cursor-shape]` Section](#editorcursor-shape-section)
89
- [`[editor.file-picker]` Section](#editorfile-picker-section)
@@ -147,6 +148,32 @@ The following statusline elements can be configured:
147148
| `version-control` | The current branch name or detached commit hash of the opened workspace |
148149
| `register` | The current selected register |
149150

151+
### `[editor.bufferline]` Section
152+
153+
For simplicity, `editor.bufferline` accepts a render mode, which will use
154+
default settings for the rest of the configuration.
155+
156+
```toml
157+
[editor]
158+
bufferline = "always"
159+
```
160+
161+
To customize the behavior of the bufferline, the `[editor.bufferline]` section
162+
must be used.
163+
164+
| Key | Description | Default |
165+
| --------- | ---------------------------------------------- | ----------- |
166+
| `show` | When to show the bufferline | `"never"` |
167+
| `context` | Whether to provide additional filepath context | `"minimal"` |
168+
169+
Example:
170+
171+
```toml
172+
[editor.bufferline]
173+
show = "always"
174+
context = "none"
175+
```
176+
150177
### `[editor.lsp]` Section
151178

152179
| Key | Description | Default |

helix-term/src/ui/editor.rs

Lines changed: 110 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,15 @@ use helix_core::{
2525
use helix_view::{
2626
annotations::diagnostics::DiagnosticFilter,
2727
document::{Mode, SCRATCH_BUFFER_NAME},
28-
editor::{CompleteAction, CursorShapeConfig},
28+
editor::{BufferLineContextMode, CompleteAction, CursorShapeConfig},
2929
graphics::{Color, CursorKind, Modifier, Rect, Style},
3030
input::{KeyEvent, MouseButton, MouseEvent, MouseEventKind},
3131
keyboard::{KeyCode, KeyModifiers},
32-
Document, Editor, Theme, View,
32+
Document, DocumentId, Editor, Theme, View,
33+
};
34+
use std::{
35+
collections::HashMap, ffi::OsString, mem::take, num::NonZeroUsize, ops, path::PathBuf, rc::Rc,
3336
};
34-
use std::{mem::take, num::NonZeroUsize, ops, path::PathBuf, rc::Rc};
3537

3638
use tui::{buffer::Buffer as Surface, text::Span};
3739

@@ -559,8 +561,12 @@ impl EditorView {
559561
}
560562

561563
/// Render bufferline at the top
562-
pub fn render_bufferline(editor: &Editor, viewport: Rect, surface: &mut Surface) {
563-
let scratch = PathBuf::from(SCRATCH_BUFFER_NAME); // default filename to use for scratch buffer
564+
pub fn render_bufferline(
565+
editor: &Editor,
566+
viewport: Rect,
567+
surface: &mut Surface,
568+
context: &BufferLineContextMode,
569+
) {
564570
surface.clear_with(
565571
viewport,
566572
editor
@@ -582,14 +588,27 @@ impl EditorView {
582588
let mut x = viewport.x;
583589
let current_doc = view!(editor).doc;
584590

591+
let fnames = match context {
592+
BufferLineContextMode::None => {
593+
let scratch = PathBuf::from(SCRATCH_BUFFER_NAME); // default filename to use for scratch buffer
594+
HashMap::<DocumentId, String>::from_iter(editor.documents().map(|doc| {
595+
(
596+
doc.id(),
597+
doc.path()
598+
.unwrap_or(&scratch)
599+
.file_name()
600+
.unwrap_or_default()
601+
.to_str()
602+
.unwrap_or_default()
603+
.to_owned(),
604+
)
605+
}))
606+
}
607+
BufferLineContextMode::Minimal => expand_fname_contexts(editor, SCRATCH_BUFFER_NAME),
608+
};
609+
585610
for doc in editor.documents() {
586-
let fname = doc
587-
.path()
588-
.unwrap_or(&scratch)
589-
.file_name()
590-
.unwrap_or_default()
591-
.to_str()
592-
.unwrap_or_default();
611+
let fname = fnames.get(&doc.id()).unwrap();
593612

594613
let style = if current_doc == doc.id() {
595614
bufferline_active
@@ -1494,10 +1513,10 @@ impl Component for EditorView {
14941513
let config = cx.editor.config();
14951514

14961515
// check if bufferline should be rendered
1497-
use helix_view::editor::BufferLine;
1498-
let use_bufferline = match config.bufferline {
1499-
BufferLine::Always => true,
1500-
BufferLine::Multiple if cx.editor.documents.len() > 1 => true,
1516+
use helix_view::editor::BufferLineRenderMode;
1517+
let use_bufferline = match config.bufferline.show {
1518+
BufferLineRenderMode::Always => true,
1519+
BufferLineRenderMode::Multiple if cx.editor.documents.len() > 1 => true,
15011520
_ => false,
15021521
};
15031522

@@ -1511,7 +1530,12 @@ impl Component for EditorView {
15111530
cx.editor.resize(editor_area);
15121531

15131532
if use_bufferline {
1514-
Self::render_bufferline(cx.editor, area.with_height(1), surface);
1533+
Self::render_bufferline(
1534+
cx.editor,
1535+
area.with_height(1),
1536+
surface,
1537+
&config.bufferline.context,
1538+
);
15151539
}
15161540

15171541
for (view, is_focused) in cx.editor.tree.views() {
@@ -1615,3 +1639,72 @@ fn canonicalize_key(key: &mut KeyEvent) {
16151639
key.modifiers.remove(KeyModifiers::SHIFT)
16161640
}
16171641
}
1642+
1643+
#[derive(Default)]
1644+
struct PathTrie {
1645+
parents: HashMap<OsString, PathTrie>,
1646+
visits: u32,
1647+
}
1648+
1649+
/// Returns a unique path ending for the current set of documents in the
1650+
/// editor. For example, documents `a/b` and `c/d` would resolve to `b` and `d`
1651+
/// respectively, while `a/b/c` and `a/d/c` would resolve to `b/c` and `d/c`
1652+
/// respectively.
1653+
fn expand_fname_contexts<'a>(editor: &'a Editor, scratch: &'a str) -> HashMap<DocumentId, String> {
1654+
let mut trie = HashMap::new();
1655+
1656+
// Build out a reverse prefix trie for all documents
1657+
for doc in editor.documents() {
1658+
let Some(path) = doc.path() else {
1659+
continue;
1660+
};
1661+
1662+
let mut current_subtrie = &mut trie;
1663+
1664+
for component in path.components().rev() {
1665+
let segment = component.as_os_str().to_os_string();
1666+
let subtrie = current_subtrie
1667+
.entry(segment)
1668+
.or_insert_with(PathTrie::default);
1669+
1670+
subtrie.visits += 1;
1671+
current_subtrie = &mut subtrie.parents;
1672+
}
1673+
}
1674+
1675+
let mut fnames = HashMap::new();
1676+
1677+
// Navigate the built reverse prefix trie to find the smallest unique path
1678+
for doc in editor.documents() {
1679+
let Some(path) = doc.path() else {
1680+
fnames.insert(doc.id(), scratch.to_owned());
1681+
continue;
1682+
};
1683+
1684+
let mut current_subtrie = &trie;
1685+
let mut built_path = vec![];
1686+
1687+
for component in path.components().rev() {
1688+
let segment = component.as_os_str().to_os_string();
1689+
let subtrie = current_subtrie
1690+
.get(&segment)
1691+
.expect("should have contained segment");
1692+
1693+
built_path.insert(0, segment);
1694+
1695+
if subtrie.visits == 1 {
1696+
fnames.insert(
1697+
doc.id(),
1698+
PathBuf::from_iter(built_path.iter())
1699+
.to_string_lossy()
1700+
.into_owned(),
1701+
);
1702+
break;
1703+
}
1704+
1705+
current_subtrie = &subtrie.parents;
1706+
}
1707+
}
1708+
1709+
fnames
1710+
}

helix-view/src/editor.rs

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,11 @@ use helix_dap as dap;
5656
use helix_lsp::lsp;
5757
use helix_stdx::path::canonicalize;
5858

59-
use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer};
59+
use serde::{
60+
de::{self, IntoDeserializer},
61+
ser::SerializeMap,
62+
Deserialize, Deserializer, Serialize, Serializer,
63+
};
6064

6165
use arc_swap::{
6266
access::{DynAccess, DynGuard},
@@ -162,6 +166,40 @@ where
162166
deserializer.deserialize_any(GutterVisitor)
163167
}
164168

169+
fn deserialize_bufferline_show_or_struct<'de, D>(deserializer: D) -> Result<BufferLine, D::Error>
170+
where
171+
D: Deserializer<'de>,
172+
{
173+
struct BufferLineVisitor;
174+
175+
impl<'de> serde::de::Visitor<'de> for BufferLineVisitor {
176+
type Value = BufferLine;
177+
178+
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
179+
write!(
180+
formatter,
181+
"a bufferline render mode or a detailed bufferline configuration"
182+
)
183+
}
184+
185+
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
186+
where
187+
E: serde::de::Error,
188+
{
189+
Ok(BufferLineRenderMode::deserialize(v.into_deserializer())?.into())
190+
}
191+
192+
fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
193+
where
194+
A: serde::de::MapAccess<'de>,
195+
{
196+
BufferLine::deserialize(de::value::MapAccessDeserializer::new(map))
197+
}
198+
}
199+
200+
deserializer.deserialize_any(BufferLineVisitor)
201+
}
202+
165203
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
166204
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
167205
pub struct GutterLineNumbersConfig {
@@ -334,6 +372,7 @@ pub struct Config {
334372
#[serde(default)]
335373
pub whitespace: WhitespaceConfig,
336374
/// Persistently display open buffers along the top
375+
#[serde(deserialize_with = "deserialize_bufferline_show_or_struct")]
337376
pub bufferline: BufferLine,
338377
/// Vertical indent width guides.
339378
pub indent_guides: IndentGuidesConfig,
@@ -671,10 +710,27 @@ impl Default for CursorShapeConfig {
671710
}
672711
}
673712

713+
/// Bufferline configuration
714+
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
715+
#[serde(rename_all = "kebab-case")]
716+
pub struct BufferLine {
717+
pub show: BufferLineRenderMode,
718+
pub context: BufferLineContextMode,
719+
}
720+
721+
impl From<BufferLineRenderMode> for BufferLine {
722+
fn from(show: BufferLineRenderMode) -> Self {
723+
Self {
724+
show,
725+
..Default::default()
726+
}
727+
}
728+
}
729+
674730
/// bufferline render modes
675731
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
676732
#[serde(rename_all = "kebab-case")]
677-
pub enum BufferLine {
733+
pub enum BufferLineRenderMode {
678734
/// Don't render bufferline
679735
#[default]
680736
Never,
@@ -684,6 +740,18 @@ pub enum BufferLine {
684740
Multiple,
685741
}
686742

743+
/// Bufferline filename context modes
744+
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
745+
#[serde(rename_all = "kebab-case")]
746+
pub enum BufferLineContextMode {
747+
/// Don't expand filenames to be unique
748+
None,
749+
750+
/// Expand filenames to the smallest unique path
751+
#[default]
752+
Minimal,
753+
}
754+
687755
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
688756
#[serde(rename_all = "kebab-case")]
689757
pub enum LineNumber {

0 commit comments

Comments
 (0)