From 627885ddeb625d13e2dcc5275998c48f3e284852 Mon Sep 17 00:00:00 2001 From: Gareth Widlansky <101901964+gerblesh@users.noreply.github.com> Date: Mon, 30 Jun 2025 22:57:20 -0700 Subject: [PATCH] feat: functional gutter --- helix-term/src/commands.rs | 10 ++- helix-term/src/ui/editor.rs | 1 + helix-term/src/ui/statusline.rs | 1 + helix-view/src/document.rs | 12 +++- helix-view/src/gutter.rs | 116 +++++++++++++++++++++++--------- 5 files changed, 105 insertions(+), 35 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 2b36e73d2..dffd336d8 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2677,12 +2677,14 @@ fn global_search(cx: &mut Context) { } let mut matches: HashMap> = HashMap::new(); + let mut lines: Vec<(PathBuf, usize)> = vec![]; for result in results { let path = result.path.clone(); let line = result.line_num; let text = result.line_content.clone(); + lines.push((path.clone(), line)); matches.entry(path).or_default().push((line, text)); } @@ -2703,6 +2705,7 @@ fn global_search(cx: &mut Context) { doc_text, matches, line_map, + lines, // TODO: actually learn how to detect encoding None, cx.editor.config.clone(), @@ -2725,7 +2728,9 @@ fn global_refactor(cx: &mut Context) { match &document_type { helix_view::document::DocumentType::File => return, - helix_view::document::DocumentType::Refactor { matches, line_map } => { + helix_view::document::DocumentType::Refactor { + matches, line_map, .. + } => { let line_ending: LineEnding = cx.editor.config.load().default_line_ending.into(); let refactor_id = doc!(cx.editor).id(); let replace_text = doc!(cx.editor).text().clone(); @@ -3276,7 +3281,8 @@ fn buffer_picker(cx: &mut Context) { &doc.document_type, helix_view::document::DocumentType::Refactor { matches: _, - line_map: _ + line_map: _, + lines: _, } ), }; diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 03ac69ca8..8bc3d8a19 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -594,6 +594,7 @@ impl EditorView { helix_view::document::DocumentType::Refactor { matches: _, line_map: _, + lines: _, } => helix_view::document::REFACTOR_BUFFER_NAME, }; diff --git a/helix-term/src/ui/statusline.rs b/helix-term/src/ui/statusline.rs index 82f9fddd0..48ee2cdcc 100644 --- a/helix-term/src/ui/statusline.rs +++ b/helix-term/src/ui/statusline.rs @@ -462,6 +462,7 @@ where helix_view::document::DocumentType::Refactor { matches: _, line_map: _, + lines: _, } => REFACTOR_BUFFER_NAME.into(), } }; diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 2e1904187..0ba1d400a 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -144,8 +144,12 @@ pub enum DocumentOpenError { pub enum DocumentType { File, Refactor { + // Filepath: list of (line_num, text) matches: HashMap>, + // (Filepath, line_num): line_num (in buffer) line_map: HashMap<(PathBuf, usize), usize>, + // List of (line_num, Filepath) in buffer order + lines: Vec<(PathBuf, usize)>, }, } @@ -748,6 +752,7 @@ impl Document { text: Rope, matches: HashMap>, line_map: HashMap<(PathBuf, usize), usize>, + lines: Vec<(PathBuf, usize)>, encoding_with_bom_info: Option<(&'static Encoding, bool)>, config: Arc>, syn_loader: Arc>, @@ -794,7 +799,11 @@ impl Document { color_swatches: None, color_swatch_controller: TaskController::new(), syn_loader, - document_type: DocumentType::Refactor { matches, line_map }, + document_type: DocumentType::Refactor { + matches, + line_map, + lines, + }, } } @@ -1813,6 +1822,7 @@ impl Document { DocumentType::Refactor { matches: _, line_map: _, + lines: _, } => false, } } diff --git a/helix-view/src/gutter.rs b/helix-view/src/gutter.rs index c2cbc0da5..e44e767b4 100644 --- a/helix-view/src/gutter.rs +++ b/helix-view/src/gutter.rs @@ -2,6 +2,7 @@ use std::fmt::Write; use helix_core::syntax::config::LanguageServerFeature; +use crate::document::DocumentType; use crate::{ editor::GutterType, graphics::{Style, UnderlineStyle}, @@ -16,6 +17,8 @@ pub type GutterFn<'doc> = Box Optio pub type Gutter = for<'doc> fn(&'doc Editor, &'doc Document, &View, &Theme, bool, usize) -> GutterFn<'doc>; +const REFACTOR_GUTTER_WIDTH: usize = 20; + impl GutterType { pub fn style<'doc>( self, @@ -151,10 +154,6 @@ pub fn line_numbers<'doc>( let last_line_in_view = view.estimate_last_doc_line(doc); - // Whether to draw the line number for the last line of the - // document or not. We only draw it if it's not an empty line. - let draw_last = text.line_to_byte(last_line_in_view) < text.len_bytes(); - let linenr = theme.get("ui.linenr"); let linenr_select = theme.get("ui.linenr.selected"); @@ -163,43 +162,93 @@ pub fn line_numbers<'doc>( .char_to_line(doc.selection(view.id).primary().cursor(text)); let line_number = editor.config().line_number; - let mode = editor.mode; + let draw_last = text.line_to_byte(last_line_in_view) < text.len_bytes(); - Box::new( - move |line: usize, selected: bool, first_visual_line: bool, out: &mut String| { - if line == last_line_in_view && !draw_last { - write!(out, "{:>1$}", '~', width).unwrap(); - Some(linenr) - } else { - use crate::{document::Mode, editor::LineNumber}; - - let relative = line_number == LineNumber::Relative - && mode != Mode::Insert - && is_focused - && current_line != line; - - let display_num = if relative { - current_line.abs_diff(line) + match &doc.document_type { + DocumentType::File => Box::new( + move |line: usize, selected: bool, first_visual_line: bool, out: &mut String| { + // Whether to draw the line number for the last line of the + // document or not. We only draw it if it's not an empty line. + let mode = editor.mode; + if line == last_line_in_view && !draw_last { + write!(out, "{:>1$}", '~', width).unwrap(); + Some(linenr) } else { - line + 1 - }; + use crate::{document::Mode, editor::LineNumber}; + let relative = line_number == LineNumber::Relative + && mode != Mode::Insert + && is_focused + && current_line != line; + + let display_num = if relative { + current_line.abs_diff(line) + } else { + line + 1 + }; + + let style = if selected && is_focused { + linenr_select + } else { + linenr + }; + + if first_visual_line { + write!(out, "{:>1$}", display_num, width).unwrap(); + } else { + write!(out, "{:>1$}", " ", width).unwrap(); + } + + first_visual_line.then_some(style) + } + }, + ), + DocumentType::Refactor { + matches: _, + line_map: _, + lines, + } => Box::new( + move |line: usize, selected: bool, first_visual_line: bool, out: &mut String| { + if let Some((file_path, line_num)) = lines.get(line) { + let gutter = format_path_line( + file_path.to_str().unwrap_or(""), + line_num + 1, + REFACTOR_GUTTER_WIDTH, + ); + write!(out, "{}", gutter).unwrap(); + } else { + write!(out, "{:>1$}", '~', REFACTOR_GUTTER_WIDTH).unwrap(); + } let style = if selected && is_focused { linenr_select } else { linenr }; - - if first_visual_line { - write!(out, "{:>1$}", display_num, width).unwrap(); - } else { - write!(out, "{:>1$}", " ", width).unwrap(); - } - + // Some(style) first_visual_line.then_some(style) - } - }, - ) + }, + ), + } +} + +fn format_path_line(path: &str, line: usize, max_width: usize) -> String { + let raw = format!("{path}:{line}"); + + if raw.len() <= max_width { + format!("{:>width$}", raw, width = max_width) + } else { + let ellipsis = "..."; + let keep = max_width.saturating_sub(ellipsis.len()); + let tail = raw + .chars() + .rev() + .take(keep) + .collect::() + .chars() + .rev() + .collect::(); + format!("{:>width$}", format!("{ellipsis}{tail}"), width = max_width) + } } /// The width of a "line-numbers" gutter @@ -208,6 +257,9 @@ pub fn line_numbers<'doc>( /// whether there is content on the last line (the `~` line), and the /// `editor.gutters.line-numbers.min-width` settings. fn line_numbers_width(view: &View, doc: &Document) -> usize { + if matches!(doc.document_type, DocumentType::Refactor { .. }) { + return REFACTOR_GUTTER_WIDTH; + } let text = doc.text(); let last_line = text.len_lines().saturating_sub(1); let draw_last = text.line_to_byte(last_line) < text.len_bytes();