Isaac Corbrey 2025-06-16 00:37:28 -04:00 committed by GitHub
commit 83f43d25ba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 195 additions and 16 deletions

View File

@ -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 |

View File

@ -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;
for doc in editor.documents() {
let fname = doc
.path()
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::<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();
.unwrap_or_default()
.to_owned(),
)
}))
}
BufferLineContextMode::Minimal => expand_fname_contexts(editor, SCRATCH_BUFFER_NAME),
};
for doc in editor.documents() {
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<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
}

View File

@ -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<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 {
@ -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,
@ -678,10 +717,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,
@ -691,6 +747,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 {