Skip to content

feat: Show filepath context in bufferline #13565

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions book/src/editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
- [`[editor]` Section](#editor-section)
- [`[editor.clipboard-provider]` Section](#editorclipboard-provider-section)
- [`[editor.statusline]` Section](#editorstatusline-section)
- [`[editor.bufferline]` Section](#editorbufferline-section)
- [`[editor.lsp]` Section](#editorlsp-section)
- [`[editor.cursor-shape]` Section](#editorcursor-shape-section)
- [`[editor.file-picker]` Section](#editorfile-picker-section)
Expand Down Expand Up @@ -147,6 +148,32 @@ The following statusline elements can be configured:
| `version-control` | The current branch name or detached commit hash of the opened workspace |
| `register` | The current selected register |

### `[editor.bufferline]` Section

For simplicity, `editor.bufferline` accepts a render mode, which will use
default settings for the rest of the configuration.

```toml
[editor]
bufferline = "always"
```

To customize the behavior of the bufferline, the `[editor.bufferline]` section
must be used.

| Key | Description | Default |
| --------- | ---------------------------------------------- | ----------- |
| `show` | When to show the bufferline | `"never"` |
| `context` | Whether to provide additional filepath context | `"minimal"` |

Example:

```toml
[editor.bufferline]
show = "always"
context = "none"
```

### `[editor.lsp]` Section

| Key | Description | Default |
Expand Down
127 changes: 110 additions & 17 deletions helix-term/src/ui/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@ use helix_core::{
use helix_view::{
annotations::diagnostics::DiagnosticFilter,
document::{Mode, SCRATCH_BUFFER_NAME},
editor::{CompleteAction, CursorShapeConfig},
editor::{BufferLineContextMode, CompleteAction, CursorShapeConfig},
graphics::{Color, CursorKind, Modifier, Rect, Style},
input::{KeyEvent, MouseButton, MouseEvent, MouseEventKind},
keyboard::{KeyCode, KeyModifiers},
Document, Editor, Theme, View,
Document, DocumentId, Editor, Theme, View,
};
use std::{
collections::HashMap, ffi::OsString, mem::take, num::NonZeroUsize, ops, path::PathBuf, rc::Rc,
};
use std::{mem::take, num::NonZeroUsize, ops, path::PathBuf, rc::Rc};

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

Expand Down Expand Up @@ -559,8 +561,12 @@ impl EditorView {
}

/// Render bufferline at the top
pub fn render_bufferline(editor: &Editor, viewport: Rect, surface: &mut Surface) {
let scratch = PathBuf::from(SCRATCH_BUFFER_NAME); // default filename to use for scratch buffer
pub fn render_bufferline(
editor: &Editor,
viewport: Rect,
surface: &mut Surface,
context: &BufferLineContextMode,
Copy link
Contributor

Choose a reason for hiding this comment

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

We don't need to add an argument to the function since the context can be accessed inside the function via the editor.

) {
surface.clear_with(
viewport,
editor
Expand All @@ -582,14 +588,27 @@ impl EditorView {
let mut x = viewport.x;
let current_doc = view!(editor).doc;

Copy link
Contributor

Choose a reason for hiding this comment

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

For example, here you can add let context = editor.config().bufferline.context.clone();

let fnames = match context {
BufferLineContextMode::None => {
let scratch = PathBuf::from(SCRATCH_BUFFER_NAME); // default filename to use for scratch buffer
HashMap::<DocumentId, String>::from_iter(editor.documents().map(|doc| {
(
doc.id(),
doc.path()
.unwrap_or(&scratch)
.file_name()
.unwrap_or_default()
.to_str()
.unwrap_or_default()
.to_owned(),
)
}))
}
BufferLineContextMode::Minimal => expand_fname_contexts(editor, SCRATCH_BUFFER_NAME),
};

for doc in editor.documents() {
let fname = doc
.path()
.unwrap_or(&scratch)
.file_name()
.unwrap_or_default()
.to_str()
.unwrap_or_default();
let fname = fnames.get(&doc.id()).unwrap();

let style = if current_doc == doc.id() {
bufferline_active
Expand Down Expand Up @@ -1494,10 +1513,10 @@ impl Component for EditorView {
let config = cx.editor.config();

// check if bufferline should be rendered
use helix_view::editor::BufferLine;
let use_bufferline = match config.bufferline {
BufferLine::Always => true,
BufferLine::Multiple if cx.editor.documents.len() > 1 => true,
use helix_view::editor::BufferLineRenderMode;
let use_bufferline = match config.bufferline.show {
BufferLineRenderMode::Always => true,
BufferLineRenderMode::Multiple if cx.editor.documents.len() > 1 => true,
_ => false,
};

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

if use_bufferline {
Self::render_bufferline(cx.editor, area.with_height(1), surface);
Self::render_bufferline(
cx.editor,
area.with_height(1),
surface,
&config.bufferline.context,
);
}

for (view, is_focused) in cx.editor.tree.views() {
Expand Down Expand Up @@ -1615,3 +1639,72 @@ fn canonicalize_key(key: &mut KeyEvent) {
key.modifiers.remove(KeyModifiers::SHIFT)
}
}

#[derive(Default)]
struct PathTrie {
parents: HashMap<OsString, PathTrie>,
visits: u32,
}

/// Returns a unique path ending for the current set of documents in the
/// editor. For example, documents `a/b` and `c/d` would resolve to `b` and `d`
/// respectively, while `a/b/c` and `a/d/c` would resolve to `b/c` and `d/c`
/// respectively.
fn expand_fname_contexts<'a>(editor: &'a Editor, scratch: &'a str) -> HashMap<DocumentId, String> {
let mut trie = HashMap::new();

// Build out a reverse prefix trie for all documents
for doc in editor.documents() {
let Some(path) = doc.path() else {
continue;
};

let mut current_subtrie = &mut trie;

for component in path.components().rev() {
let segment = component.as_os_str().to_os_string();
let subtrie = current_subtrie
.entry(segment)
.or_insert_with(PathTrie::default);

subtrie.visits += 1;
current_subtrie = &mut subtrie.parents;
}
}

let mut fnames = HashMap::new();

// Navigate the built reverse prefix trie to find the smallest unique path
for doc in editor.documents() {
let Some(path) = doc.path() else {
fnames.insert(doc.id(), scratch.to_owned());
continue;
};

let mut current_subtrie = &trie;
let mut built_path = vec![];

for component in path.components().rev() {
let segment = component.as_os_str().to_os_string();
let subtrie = current_subtrie
.get(&segment)
.expect("should have contained segment");

built_path.insert(0, segment);

if subtrie.visits == 1 {
fnames.insert(
doc.id(),
PathBuf::from_iter(built_path.iter())
.to_string_lossy()
.into_owned(),
);
break;
}

current_subtrie = &subtrie.parents;
}
}

fnames
}
72 changes: 70 additions & 2 deletions helix-view/src/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,11 @@ use helix_dap as dap;
use helix_lsp::lsp;
use helix_stdx::path::canonicalize;

use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer};
use serde::{
de::{self, IntoDeserializer},
ser::SerializeMap,
Deserialize, Deserializer, Serialize, Serializer,
};

use arc_swap::{
access::{DynAccess, DynGuard},
Expand Down Expand Up @@ -162,6 +166,40 @@ where
deserializer.deserialize_any(GutterVisitor)
}

fn deserialize_bufferline_show_or_struct<'de, D>(deserializer: D) -> Result<BufferLine, D::Error>
where
D: Deserializer<'de>,
{
struct BufferLineVisitor;

impl<'de> serde::de::Visitor<'de> for BufferLineVisitor {
type Value = BufferLine;

fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
formatter,
"a bufferline render mode or a detailed bufferline configuration"
)
}

fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(BufferLineRenderMode::deserialize(v.into_deserializer())?.into())
}

fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
BufferLine::deserialize(de::value::MapAccessDeserializer::new(map))
}
}

deserializer.deserialize_any(BufferLineVisitor)
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
pub struct GutterLineNumbersConfig {
Expand Down Expand Up @@ -334,6 +372,7 @@ pub struct Config {
#[serde(default)]
pub whitespace: WhitespaceConfig,
/// Persistently display open buffers along the top
#[serde(deserialize_with = "deserialize_bufferline_show_or_struct")]
pub bufferline: BufferLine,
/// Vertical indent width guides.
pub indent_guides: IndentGuidesConfig,
Expand Down Expand Up @@ -671,10 +710,27 @@ impl Default for CursorShapeConfig {
}
}

/// Bufferline configuration
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct BufferLine {
pub show: BufferLineRenderMode,
pub context: BufferLineContextMode,
}

impl From<BufferLineRenderMode> for BufferLine {
fn from(show: BufferLineRenderMode) -> Self {
Self {
show,
..Default::default()
}
}
}

/// bufferline render modes
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum BufferLine {
pub enum BufferLineRenderMode {
/// Don't render bufferline
#[default]
Never,
Expand All @@ -684,6 +740,18 @@ pub enum BufferLine {
Multiple,
}

/// Bufferline filename context modes
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum BufferLineContextMode {
/// Don't expand filenames to be unique
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: I would rephrase this from what it doesn't do to what it does. For example, "Only show filename" or something along those lines

None,

/// Expand filenames to the smallest unique path
#[default]
Minimal,
}

#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum LineNumber {
Expand Down