Commit bfc17d6
Changed files (6)
modules/dev/editor/helix/patches/11700-search-position.patch
@@ -0,0 +1,403 @@
+diff --git a/book/src/editor.md b/book/src/editor.md
+index d2b5016b..b7700866 100644
+--- a/book/src/editor.md
++++ b/book/src/editor.md
+@@ -173,6 +173,7 @@ ### `[editor.statusline]` Section
+ | `diagnostics` | The number of warnings and/or errors |
+ | `workspace-diagnostics` | The number of warnings and/or errors on workspace |
+ | `selections` | The primary selection index out of the number of active selections |
++| `search-position` | Current search match and total matches in the view `[<current>/<total>]` |
+ | `primary-selection-length` | The number of characters currently in primary selection |
+ | `position` | The cursor position |
+ | `position-percentage` | The cursor position as a percentage of the total number of lines |
+@@ -349,6 +350,7 @@ ### `[editor.search]` Section
+ |--|--|---------|
+ | `smart-case` | Enable smart case regex searching (case-insensitive unless pattern contains upper case characters) | `true` |
+ | `wrap-around`| Whether the search should wrap after depleting the matches | `true` |
++| `max-matches`| Maximum number of matches that will be counted for the denominator of `search-position`. Possible values are integers or `"none"` for no limit. | `100` |
+
+ ### `[editor.whitespace]` Section
+
+diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
+index cd11400c..507cb75f 100644
+--- a/helix-term/src/commands.rs
++++ b/helix-term/src/commands.rs
+@@ -49,8 +49,8 @@
+ Selection, SmallVec, Syntax, Tendril, Transaction,
+ };
+ use helix_view::{
+- document::{FormatterError, Mode, SCRATCH_BUFFER_NAME},
+- editor::{Action, Motion},
++ document::{FormatterError, Mode, SearchMatch, SearchMatchLimit, SCRATCH_BUFFER_NAME},
++ editor::{Action, Motion, OptionToml, SearchConfig},
+ expansion,
+ info::Info,
+ input::KeyEvent,
+@@ -2276,12 +2276,13 @@ fn search_impl(
+ movement: Movement,
+ direction: Direction,
+ scrolloff: usize,
+- wrap_around: bool,
++ search_config: &SearchConfig,
+ show_warnings: bool,
+ ) {
+ let (view, doc) = current!(editor);
+ let text = doc.text().slice(..);
+ let selection = doc.selection(view.id);
++ let wrap_around = search_config.wrap_around;
+
+ // Get the right side of the primary block cursor for forward search, or the
+ // grapheme before the start of the selection for reverse search.
+@@ -2302,54 +2303,126 @@ fn search_impl(
+ // it out, we need to add it back to the position of the selection.
+ let doc = doc!(editor).text().slice(..);
+
+- // use find_at to find the next match after the cursor, loop around the end
++ let mut all_matches = regex.find_iter(doc.regex_input()).enumerate().peekable();
++
++ if all_matches.peek().is_none() {
++ if show_warnings {
++ editor.set_error("No matches");
++ }
++ return;
++ }
++
++ // We will get the number of the current match and total matches from
++ // `all_matches`. So, here we look in the iterator until we find exactly
++ // the match we were looking for (either after or behind `start`). In the
++ // `Backward` case, in particular, we need to look the match ahead to know
++ // if this is the one we need.
++
+ // Careful, `Regex` uses `bytes` as offsets, not character indices!
+ let mut mat = match direction {
+- Direction::Forward => regex.find(doc.regex_input_at_bytes(start..)),
+- Direction::Backward => regex.find_iter(doc.regex_input_at_bytes(..start)).last(),
++ Direction::Forward => all_matches.by_ref().find(|&(_, m)| m.start() >= start),
++ Direction::Backward => {
++ let one_behind = std::iter::once(None).chain(all_matches.by_ref().map(Some));
++ one_behind
++ .zip(regex.find_iter(doc.regex_input()))
++ .find(|&(_, m1)| m1.start() >= start)
++ .map(|(m0, _)| m0)
++ .unwrap_or(None)
++ }
+ };
+
+- if mat.is_none() {
+- if wrap_around {
+- mat = match direction {
+- Direction::Forward => regex.find(doc.regex_input()),
+- Direction::Backward => regex.find_iter(doc.regex_input_at_bytes(start..)).last(),
+- };
++ if mat.is_none() && !wrap_around {
++ if show_warnings {
++ editor.set_error("No more matches");
+ }
++ return;
++ }
++
++ // If we didn't find a match before, lets wrap the search.
++ if mat.is_none() {
+ if show_warnings {
+- if wrap_around && mat.is_some() {
+- editor.set_status("Wrapped around document");
++ editor.set_status("Wrapped around document");
++ }
++
++ let doc = doc!(editor).text().slice(..);
++ all_matches = regex.find_iter(doc.regex_input()).enumerate().peekable();
++ mat = match direction {
++ Direction::Forward => all_matches.by_ref().next(),
++ Direction::Backward => all_matches.by_ref().last(),
++ };
++ }
++
++ let (idx, mat) = mat.unwrap();
++ let match_count = match search_config.max_matches {
++ OptionToml::None => match all_matches.last() {
++ None => SearchMatchLimit::Limitless(idx + 1),
++ Some((last_idx, _)) => SearchMatchLimit::Limitless(last_idx + 1),
++ },
++ OptionToml::Some(max) => {
++ if all_matches.peek().is_none() {
++ // Case #1: If we consumed `all_matches`, it means that we have
++ // the last match in `mat`. Hence, we know exactly how many
++ // matches there are. To respect the `max` option, if it goes
++ // beyong `max`, we limit the counter to `max`.
++ if idx + 1 > max {
++ SearchMatchLimit::Limited(max)
++ } else {
++ SearchMatchLimit::Limitless(idx + 1)
++ }
+ } else {
+- editor.set_error("No more matches");
++ // Case #2: If we are here, we have at least one match in
++ // `all_matches`. We need to find the last match that's
++ // less than `max`. If we find it, we simply return it as a
++ // `Limitless` denominator because it doesn't go beyong the
++ // user option. The two remaining cases are `last_idx == max`
++ // and `None` (when the remaining matches are all greater than
++ // `max`) for which we return a `Limited` denominator.
++ match all_matches.take_while(|(idx, _)| idx <= &max).last() {
++ Some((last_idx, _)) if last_idx < max => {
++ SearchMatchLimit::Limitless(last_idx + 1)
++ }
++ _ => SearchMatchLimit::Limited(max),
++ }
+ }
+ }
+- }
++ };
+
++ // Move the cursor to the match.
+ let (view, doc) = current!(editor);
+ let text = doc.text().slice(..);
+ let selection = doc.selection(view.id);
+
+- if let Some(mat) = mat {
+- let start = text.byte_to_char(mat.start());
+- let end = text.byte_to_char(mat.end());
++ let start = text.byte_to_char(mat.start());
++ let end = text.byte_to_char(mat.end());
+
+- if end == 0 {
+- // skip empty matches that don't make sense
+- return;
+- }
+-
+- // Determine range direction based on the primary range
+- let primary = selection.primary();
+- let range = Range::new(start, end).with_direction(primary.direction());
++ if end == 0 {
++ // skip empty matches that don't make sense
++ return;
++ }
+
+- let selection = match movement {
+- Movement::Extend => selection.clone().push(range),
+- Movement::Move => selection.clone().replace(selection.primary_index(), range),
+- };
++ // Determine range direction based on the primary range
++ let primary = selection.primary();
++ let range = Range::new(start, end).with_direction(primary.direction());
+
+- doc.set_selection(view.id, selection);
+- view.ensure_cursor_in_view_center(doc, scrolloff);
++ let selection = match movement {
++ Movement::Extend => selection.clone().push(range),
++ Movement::Move => selection.clone().replace(selection.primary_index(), range),
+ };
++
++ doc.set_selection(view.id, selection);
++ view.ensure_cursor_in_view_center(doc, scrolloff);
++
++ // Set the index of this match and total number of matchs in the doc. It's
++ // important to set it after `set_selection` since that method resets the
++ // last match position.
++ let (view, doc) = current!(editor);
++ doc.set_last_search_match(
++ view.id,
++ SearchMatch {
++ idx: idx + 1,
++ count: match_count,
++ },
++ );
+ }
+
+ fn search_completions(cx: &mut Context, reg: Option<char>) -> Vec<String> {
+@@ -2373,7 +2446,6 @@ fn searcher(cx: &mut Context, direction: Direction) {
+ let reg = cx.register.unwrap_or('/');
+ let config = cx.editor.config();
+ let scrolloff = config.scrolloff;
+- let wrap_around = config.search.wrap_around;
+ let movement = if cx.editor.mode() == Mode::Select {
+ Movement::Extend
+ } else {
+@@ -2406,7 +2478,7 @@ fn searcher(cx: &mut Context, direction: Direction) {
+ movement,
+ direction,
+ scrolloff,
+- wrap_around,
++ &config.search,
+ false,
+ );
+ },
+@@ -2427,7 +2499,6 @@ fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Dir
+ } else {
+ false
+ };
+- let wrap_around = search_config.wrap_around;
+ let is_crlf = doc!(cx.editor).line_ending == LineEnding::Crlf;
+ if let Ok(regex) = rope::RegexBuilder::new()
+ .syntax(
+@@ -2445,7 +2516,7 @@ fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Dir
+ movement,
+ direction,
+ scrolloff,
+- wrap_around,
++ search_config,
+ true,
+ );
+ }
+diff --git a/helix-term/src/commands/engine/steel/mod.rs b/helix-term/src/commands/engine/steel/mod.rs
+index 8a166638..a8f64986 100644
+--- a/helix-term/src/commands/engine/steel/mod.rs
++++ b/helix-term/src/commands/engine/steel/mod.rs
+@@ -3104,6 +3104,7 @@ fn search(&self, smart_case: bool, wrap_around: bool) {
+ app_config.editor.search = SearchConfig {
+ smart_case,
+ wrap_around,
++ ..SearchConfig::default()
+ };
+ self.store_config(app_config);
+ }
+diff --git a/helix-term/src/ui/statusline.rs b/helix-term/src/ui/statusline.rs
+index acca850d..e71b57d2 100644
+--- a/helix-term/src/ui/statusline.rs
++++ b/helix-term/src/ui/statusline.rs
+@@ -3,7 +3,7 @@
+ use helix_lsp::lsp::DiagnosticSeverity;
+ use helix_view::document::DEFAULT_LANGUAGE_NAME;
+ use helix_view::{
+- document::{Mode, SCRATCH_BUFFER_NAME},
++ document::{Mode, SearchMatch, SearchMatchLimit, SCRATCH_BUFFER_NAME},
+ graphics::Rect,
+ theme::Style,
+ Document, Editor,
+@@ -184,6 +184,7 @@ fn get_render_function<'a, F>(
+ helix_view::editor::StatusLineElement::Register => render_register,
+ helix_view::editor::StatusLineElement::CurrentWorkingDirectory => render_cwd,
+ helix_view::editor::StatusLineElement::CodeActionHint => render_code_action_hint,
++ helix_view::editor::StatusLineElement::SearchPosition => render_search_position,
+ #[cfg(feature = "steel")]
+ _ => unreachable!(),
+ }
+@@ -633,3 +634,17 @@ fn render_code_action_hint<'a, F>(context: &mut RenderContext<'a>, write: F)
+ write(context, " ⋮ ".into())
+ }
+ }
++
++fn render_search_position<'a, F>(context: &mut RenderContext<'a>, write: F)
++where
++ F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy,
++{
++ let doc = get_doc(context);
++ if let Some(SearchMatch { idx, count }) = doc.get_last_search_match(context.view_id) {
++ let count_str = match count {
++ SearchMatchLimit::Limitless(count) => format!("{}", count),
++ SearchMatchLimit::Limited(max) => format!(">{}", max),
++ };
++ write(context, format!(" [{}/{}] ", idx, count_str).into());
++ }
++}
+diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs
+index b17fd230..6e5fbd98 100644
+--- a/helix-view/src/document.rs
++++ b/helix-view/src/document.rs
+@@ -139,12 +139,28 @@ pub enum DocumentOpenError {
+ IoError(#[from] io::Error),
+ }
+
++#[derive(Debug, Clone, Copy)]
++pub enum SearchMatchLimit {
++ Limitless(usize),
++ Limited(usize),
++}
++
++#[derive(Debug, Clone, Copy)]
++pub struct SearchMatch {
++ /// nth match from the beginning of the document.
++ pub idx: usize,
++ /// Total number of matches in the document.
++ pub count: SearchMatchLimit,
++}
++
+ pub struct Document {
+ pub(crate) id: DocumentId,
+ text: Rope,
+ selections: HashMap<ViewId, Selection>,
+ view_data: HashMap<ViewId, ViewData>,
+ pub active_snippet: Option<ActiveSnippet>,
++ /// Current search information.
++ last_search_match: HashMap<ViewId, SearchMatch>,
+
+ /// Inlay hints annotations for the document, by view.
+ ///
+@@ -832,6 +848,7 @@ pub fn from(
+ text,
+ selections: HashMap::default(),
+ inlay_hints: HashMap::default(),
++ last_search_match: HashMap::default(),
+ inlay_hints_oudated: false,
+ view_data: Default::default(),
+ indent_style: DEFAULT_INDENT,
+@@ -1494,6 +1511,8 @@ pub fn set_language_by_language_id(
+
+ /// Select text within the [`Document`].
+ pub fn set_selection(&mut self, view_id: ViewId, selection: Selection) {
++ self.last_search_match.remove(&view_id);
++
+ // TODO: use a transaction?
+ self.selections
+ .insert(view_id, selection.ensure_invariants(self.text().slice(..)));
+@@ -1503,6 +1522,14 @@ pub fn set_selection(&mut self, view_id: ViewId, selection: Selection) {
+ })
+ }
+
++ pub fn set_last_search_match(&mut self, view_id: ViewId, search_match: SearchMatch) {
++ self.last_search_match.insert(view_id, search_match);
++ }
++
++ pub fn get_last_search_match(&self, view_id: ViewId) -> Option<SearchMatch> {
++ self.last_search_match.get(&view_id).copied()
++ }
++
+ /// Find the origin selection of the text in a document, i.e. where
+ /// a single cursor would go if it were on the first grapheme. If
+ /// the text is empty, returns (0, 0).
+diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs
+index 263a8542..bfa9acd7 100644
+--- a/helix-view/src/editor.rs
++++ b/helix-view/src/editor.rs
+@@ -687,6 +687,14 @@ fn default() -> Self {
+ }
+ }
+
++#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
++#[serde(rename_all = "kebab-case", deny_unknown_fields)]
++pub enum OptionToml<T> {
++ None,
++ #[serde(untagged)]
++ Some(T),
++}
++
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+ #[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
+ pub struct SearchConfig {
+@@ -694,6 +702,8 @@ pub struct SearchConfig {
+ pub smart_case: bool,
+ /// Whether the search should wrap after depleting the matches. Default to true.
+ pub wrap_around: bool,
++ /// Maximum number of counted matches when searching in a document. `None` means no limit. Default to 100.
++ pub max_matches: OptionToml<usize>,
+ }
+
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+@@ -853,6 +863,9 @@ pub enum StatusLineElement {
+ /// Indicator for selected register
+ Register,
+
++ /// Search index and count
++ SearchPosition,
++
+ /// The base of current working directory
+ CurrentWorkingDirectory,
+
+@@ -1327,6 +1340,7 @@ fn default() -> Self {
+ Self {
+ wrap_around: true,
+ smart_case: true,
++ max_matches: OptionToml::Some(100),
+ }
+ }
+ }
modules/dev/editor/helix/patches/12173-clickable-bufferline.patch
@@ -0,0 +1,139 @@
+diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
+index 05cf7769..4f5081bf 100644
+--- a/helix-term/src/ui/editor.rs
++++ b/helix-term/src/ui/editor.rs
+@@ -43,6 +43,7 @@ pub struct EditorView {
+ pub(crate) last_insert: (commands::MappableCommand, Vec<InsertEvent>),
+ pub(crate) completion: Option<Completion>,
+ spinners: ProgressSpinners,
++ bufferline_info: BufferLineInfo,
+ /// Tracks if the terminal window is focused by reaction to terminal focus events
+ terminal_focused: bool,
+ }
+@@ -67,6 +68,7 @@ pub fn new(keymaps: Keymaps) -> Self {
+ last_insert: (commands::MappableCommand::normal_mode, Vec::new()),
+ completion: None,
+ spinners: ProgressSpinners::default(),
++ bufferline_info: BufferLineInfo::default(),
+ terminal_focused: true,
+ }
+ }
+@@ -683,7 +685,7 @@ pub fn tabstop_highlights(doc: &Document, theme: &Theme) -> Option<OverlayHighli
+ }
+
+ /// Render bufferline at the top
+- pub fn render_bufferline(editor: &Editor, viewport: Rect, surface: &mut Surface) {
++ pub fn render_bufferline(&mut self, 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,
+@@ -706,6 +708,8 @@ pub fn render_bufferline(editor: &Editor, viewport: Rect, surface: &mut Surface)
+ let mut x = viewport.x;
+ let current_doc = view!(editor).doc;
+
++ self.bufferline_info.clear();
++
+ for doc in editor.documents() {
+ let fname = doc
+ .path()
+@@ -725,9 +729,14 @@ pub fn render_bufferline(editor: &Editor, viewport: Rect, surface: &mut Surface)
+ let used_width = viewport.x.saturating_sub(x);
+ let rem_width = surface.area.width.saturating_sub(used_width);
+
++ let start_x = x;
+ x = surface
+ .set_stringn(x, viewport.y, &text, rem_width as usize, style)
+ .0;
++ let end_x = x.min(surface.area.right());
++
++ self.bufferline_info
++ .add_buffer_info(doc.id(), start_x..end_x);
+
+ if x >= surface.area.right() {
+ break;
+@@ -1396,6 +1405,14 @@ fn handle_mouse_event(
+ MouseEventKind::Down(MouseButton::Left) => {
+ let editor = &mut cxt.editor;
+
++ if is_bufferline_visible(editor) && row == 0 {
++ if let Some(buffer_info) = self.bufferline_info.get_clicked_buffer(column) {
++ editor.switch(buffer_info.document_id, helix_view::editor::Action::Replace);
++ }
++
++ return EventResult::Consumed(None);
++ }
++
+ if let Some((pos, view_id)) = pos_and_view(editor, row, column, true) {
+ editor.focus(view_id);
+
+@@ -1778,12 +1795,7 @@ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
+ surface.set_style(area, cx.editor.theme.get("ui.background"));
+ let config = cx.editor.config();
+
+- // check if bufferline should be rendered
+- let use_bufferline = match config.bufferline {
+- BufferLine::Always => true,
+- BufferLine::Multiple if cx.editor.documents.len() > 1 => true,
+- _ => false,
+- };
++ let use_bufferline = is_bufferline_visible(cx.editor);
+
+ let mut area = area;
+
+@@ -1811,7 +1823,7 @@ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
+ 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);
+ }
+
+ let views: Vec<(ViewId, bool)> = {
+@@ -1936,6 +1948,47 @@ fn cursor(&self, _area: Rect, editor: &Editor) -> (Option<Position>, CursorKind)
+ }
+ }
+
++#[derive(Debug, Default)]
++struct BufferLineInfo {
++ visible_buffers: Vec<BufferInfo>,
++}
++
++impl BufferLineInfo {
++ fn clear(&mut self) {
++ self.visible_buffers.clear();
++ }
++
++ fn add_buffer_info(&mut self, document_id: DocumentId, columns: std::ops::Range<u16>) {
++ self.visible_buffers.push(BufferInfo {
++ document_id,
++ columns,
++ });
++ }
++
++ fn get_clicked_buffer(&self, column: u16) -> Option<&BufferInfo> {
++ self.visible_buffers
++ .iter()
++ .find(|cell| cell.columns.contains(&column))
++ }
++}
++
++#[derive(Debug)]
++struct BufferInfo {
++ document_id: DocumentId,
++ // The bufferline column span used to show the document name
++ columns: std::ops::Range<u16>,
++}
++
++fn is_bufferline_visible(editor: &Editor) -> bool {
++ let config = editor.config();
++
++ match config.bufferline {
++ BufferLine::Always => true,
++ BufferLine::Multiple => editor.documents.len() > 1,
++ BufferLine::Never => false,
++ }
++}
++
+ fn canonicalize_key(key: &mut KeyEvent) {
+ if let KeyEvent {
+ code: KeyCode::Char(_),
modules/dev/editor/helix/patches/13113-unique-buffer-names.patch
@@ -0,0 +1,106 @@
+diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
+index 4f5081bf..57354e61 100644
+--- a/helix-term/src/ui/editor.rs
++++ b/helix-term/src/ui/editor.rs
+@@ -32,7 +32,13 @@
+ keyboard::{KeyCode, KeyModifiers},
+ Document, DocumentId, Editor, Theme, View, ViewId,
+ };
+-use std::{mem::take, num::NonZeroUsize, ops, path::PathBuf, rc::Rc};
++use std::{
++ mem::take,
++ num::NonZeroUsize,
++ ops,
++ path::{PathBuf, MAIN_SEPARATOR, MAIN_SEPARATOR_STR},
++ rc::Rc,
++};
+
+ use tui::{buffer::Buffer as Surface, text::Span};
+
+@@ -686,7 +692,6 @@ pub fn tabstop_highlights(doc: &Document, theme: &Theme) -> Option<OverlayHighli
+
+ /// Render bufferline at the top
+ pub fn render_bufferline(&mut self, 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
+@@ -711,13 +716,7 @@ pub fn render_bufferline(&mut self, editor: &Editor, viewport: Rect, surface: &m
+ self.bufferline_info.clear();
+
+ for doc in editor.documents() {
+- let fname = doc
+- .path()
+- .unwrap_or(&scratch)
+- .file_name()
+- .unwrap_or_default()
+- .to_str()
+- .unwrap_or_default();
++ let fname = Self::make_document_name(doc, editor);
+
+ let style = if current_doc == doc.id() {
+ bufferline_active
+@@ -873,6 +872,63 @@ fn draw_element(
+ }
+ }
+
++ fn make_document_name(doc: &Document, editor: &Editor) -> String {
++ let scratch = PathBuf::from(SCRATCH_BUFFER_NAME); // default filename to use for scratch buffer
++
++ let paths: Vec<String> = editor
++ .documents()
++ .map(|d| {
++ d.path()
++ .unwrap_or(&scratch)
++ .to_str()
++ .unwrap_or_default()
++ .to_string()
++ })
++ .collect();
++
++ let components: Vec<Vec<String>> = paths
++ .iter()
++ .map(|p| p.split(MAIN_SEPARATOR).map(String::from).collect())
++ .collect();
++
++ let doc_path = doc
++ .path()
++ .unwrap_or(&scratch)
++ .to_str()
++ .unwrap_or_default()
++ .to_string();
++
++ let doc_index = paths.iter().position(|p| p == &doc_path).unwrap();
++ let doc_components_len = components[doc_index].len();
++
++ let mut k = 1;
++
++ loop {
++ let start = doc_components_len.saturating_sub(k);
++ let curr_doc: Vec<&str> = components[doc_index][start..]
++ .iter()
++ .map(|s| s.as_str())
++ .collect();
++
++ let count = components
++ .iter()
++ .enumerate()
++ .filter(|&(index, _)| index != doc_index)
++ .filter(|&(_, parts)| {
++ let len = parts.len();
++ let start = len.saturating_sub(k);
++ parts[start..] == curr_doc
++ })
++ .count();
++
++ if count == 0 {
++ return curr_doc.join(MAIN_SEPARATOR_STR);
++ }
++
++ k += 1;
++ }
++ }
++
+ pub fn render_gutter<'d>(
+ editor: &'d Editor,
+ doc: &'d Document,
modules/dev/editor/helix/patches/15266-prompt-history-search.patch
@@ -0,0 +1,103 @@
+diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs
+index 926f33a3..f3124c63 100644
+--- a/helix-term/src/ui/prompt.rs
++++ b/helix-term/src/ui/prompt.rs
+@@ -44,6 +44,8 @@ pub struct Prompt {
+ selection: Option<usize>,
+ history_register: Option<char>,
+ history_pos: Option<usize>,
++ history_pattern: Option<String>,
++ user_edited: bool,
+ completion_fn: CompletionFn,
+ callback_fn: CallbackFn,
+ pub doc_fn: DocFn,
+@@ -100,6 +102,8 @@ pub fn new(
+ selection: None,
+ history_register,
+ history_pos: None,
++ history_pattern: None,
++ user_edited: true,
+ completion_fn: Box::new(completion_fn),
+ callback_fn: Box::new(callback_fn),
+ doc_fn: Box::new(|_| None),
+@@ -346,27 +350,40 @@ pub fn change_history(
+ register: char,
+ direction: CompletionDirection,
+ ) {
++ if self.line.is_empty() {
++ self.history_pattern = None;
++ self.history_pos = None;
++ } else if self.user_edited {
++ self.history_pattern = Some(self.line.clone());
++ self.history_pos = None;
++ }
+ (self.callback_fn)(cx, &self.line, PromptEvent::Abort);
+- let mut values = match cx.editor.registers.read(register, cx.editor) {
+- Some(values) if values.len() > 0 => values.rev(),
++
++ let values: Vec<_> = match cx.editor.registers.read(register, cx.editor) {
++ Some(values) if values.len() > 0 => {
++ if let Some(pattern) = &self.history_pattern {
++ values.filter(|the| the.contains(pattern)).rev().collect()
++ } else {
++ values.rev().collect()
++ }
++ }
+ _ => return,
+ };
++ if values.is_empty() {
++ return;
++ }
+
+ let end = values.len().saturating_sub(1);
+
+ let index = match direction {
+ CompletionDirection::Forward => self.history_pos.map_or(0, |i| i + 1),
+- CompletionDirection::Backward => self
+- .history_pos
+- .unwrap_or_else(|| values.len())
+- .saturating_sub(1),
++ CompletionDirection::Backward => {
++ self.history_pos.unwrap_or(values.len()).saturating_sub(1)
++ }
+ }
+ .min(end);
+
+- self.line = values.nth(index).unwrap().to_string();
+- // Appease the borrow checker.
+- drop(values);
+-
++ self.line = values.into_iter().nth(index).unwrap().to_string();
+ self.history_pos = Some(index);
+
+ self.move_end();
+@@ -627,6 +644,7 @@ fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult {
+ compositor.remove(id);
+ })));
+
++ let mut user_edited = true;
+ match event {
+ ctrl!('c') | key!(Esc) => {
+ (self.callback_fn)(cx, &self.line, PromptEvent::Abort);
+@@ -713,11 +731,13 @@ fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult {
+ }
+ }
+ ctrl!('p') | key!(Up) => {
++ user_edited = false;
+ if let Some(register) = self.history_register {
+ self.change_history(cx, register, CompletionDirection::Backward);
+ }
+ }
+ ctrl!('n') | key!(Down) => {
++ user_edited = false;
+ if let Some(register) = self.history_register {
+ self.change_history(cx, register, CompletionDirection::Forward);
+ }
+@@ -765,6 +785,7 @@ fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult {
+ }
+ _ => (),
+ };
++ self.user_edited = user_edited;
+
+ EventResult::Consumed(None)
+ }
modules/dev/editor/helix/default.nix
@@ -36,7 +36,34 @@
programs.helix = {
enable = true;
- package = pkgs.nur.repos.hpcesia.steelix;
+ package = pkgs.nur.repos.hpcesia.steelix.override {
+ steelix-unwrapped =
+ (pkgs.callPackage "${inputs.nur-hpcesia}/pkgs/steelix/unwrapped.nix" {}).overrideAttrs
+ (_: prev: {
+ patches =
+ prev.patches
+ ++ [
+ # Breadcrumb navigation bar.
+ # https://github.com/helix-editor/helix/pull/15573
+ ./patches/15573-breadcrumb.patch
+ # Search position statusline element and `search.max-matches`.
+ # Rebased on top of the breadcrumb patch.
+ # https://github.com/helix-editor/helix/pull/11700
+ ./patches/11700-search-position.patch
+ # Click a bufferline entry to switch buffers.
+ # Rebased on top of the breadcrumb patch.
+ # https://github.com/helix-editor/helix/pull/12173
+ ./patches/12173-clickable-bufferline.patch
+ # Disambiguate same-named buffers by prepending path components.
+ # Rebased on top of the breadcrumb and clickable-bufferline patches.
+ # https://github.com/helix-editor/helix/pull/13113
+ ./patches/13113-unique-buffer-names.patch
+ # Prompt history search by typed input.
+ # https://github.com/helix-editor/helix/pull/15266
+ ./patches/15266-prompt-history-search.patch
+ ];
+ });
+ };
settings = {
editor = {
cursorline = true;
@@ -56,6 +83,7 @@
"file-type"
"file-encoding"
"separator"
+ "search-position"
"position"
"total-line-numbers"
];
@@ -76,6 +104,7 @@
trim-trailing-whitespace = true;
inline-diagnostics.cursor-line = "warning";
end-of-line-diagnostics = "error";
+ breadcrumb.enable = true;
lsp = {
display-inlay-hints = true;
inlay-hints-length-limit = 16;