diff --git a/book/src/editor.md b/book/src/editor.md index 00db71d27..a9e8137ec 100644 --- a/book/src/editor.md +++ b/book/src/editor.md @@ -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) @@ -148,6 +149,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. Can be `always`, `never` or `multiple` (only shown if more than one buffer is in use). | `"never"` | +| `context` | Whether to provide additional filepath context. Can be `minimal` or `none`. | `"minimal"` | + +Example: + +```toml +[editor.bufferline] +show = "always" +context = "none" +``` + ### `[editor.lsp]` Section | Key | Description | Default | diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 9343d55d4..dca90e49e 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -29,9 +29,11 @@ use helix_view::{ 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}; @@ -560,7 +562,6 @@ 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 surface.clear_with( viewport, editor @@ -582,14 +583,28 @@ impl EditorView { let mut x = viewport.x; let current_doc = view!(editor).doc; + use helix_view::editor::BufferLineContextMode; + let fnames = match editor.config().bufferline.context.clone() { + BufferLineContextMode::None => { + let scratch = PathBuf::from(SCRATCH_BUFFER_NAME); // default filename to use for scratch buffer + HashMap::::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 @@ -1494,10 +1509,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 => 1 < cx.editor.documents.len(), _ => false, }; @@ -1615,3 +1630,72 @@ fn canonicalize_key(key: &mut KeyEvent) { key.modifiers.remove(KeyModifiers::SHIFT) } } + +#[derive(Default)] +struct PathTrie { + parents: HashMap, + 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 { + 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 = ≜ + 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 +} diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index bc811b88b..f865eeeaf 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -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}, @@ -162,6 +166,40 @@ where deserializer.deserialize_any(GutterVisitor) } +fn deserialize_bufferline_show_or_struct<'de, D>(deserializer: D) -> Result +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(self, v: &str) -> Result + where + E: serde::de::Error, + { + Ok(BufferLineRenderMode::deserialize(v.into_deserializer())?.into()) + } + + fn visit_map(self, map: A) -> Result + 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 { @@ -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, @@ -674,10 +713,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 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, @@ -687,6 +743,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 { + /// Only show the filename + 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 {