mirror of https://github.com/helix-editor/helix
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.pull/13565/head
parent
ab97585b69
commit
bc0f718df5
|
@ -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 |
|
||||
|
|
|
@ -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::<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
|
||||
|
@ -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 = ≜
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
@ -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<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,
|
||||
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue