From d9727082b3ff4a4645489baa3c1e2fef0d694b7d Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+nikitarevenco@users.noreply.github.com> Date: Thu, 7 Nov 2024 12:20:41 +0000 Subject: [PATCH] feat: add config option to make statusline unobtrusive --- helix-term/src/application.rs | 13 ++- helix-term/src/commands.rs | 83 +++++++++++------- helix-term/src/commands/lsp.rs | 13 ++- helix-term/src/commands/typed.rs | 56 ++++++++---- helix-term/src/ui/editor.rs | 17 ++-- helix-term/src/ui/mod.rs | 2 +- helix-term/src/ui/statusline.rs | 6 +- helix-view/src/editor.rs | 21 +++-- helix-view/src/gutter.rs | 6 +- helix-view/src/handlers/dap.rs | 3 +- helix-view/src/lib.rs | 4 +- helix-view/src/view.rs | 142 +++++++++++++++++++++---------- 12 files changed, 242 insertions(+), 124 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index a567815fc..add959c40 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -105,6 +105,7 @@ impl Application { use helix_view::editor::Action; + let unobtrusive_statusline = config.editor.statusline.unobtrusive; let mut theme_parent_dirs = vec![helix_loader::config_dir()]; theme_parent_dirs.extend(helix_loader::runtime_dirs().iter().cloned()); let theme_loader = std::sync::Arc::new(theme::Loader::new(&theme_parent_dirs)); @@ -221,7 +222,7 @@ impl Application { // align the view to center after all files are loaded, // does not affect views without pos since it is at the top let (view, doc) = current!(editor); - align_view(doc, view, Align::Center); + align_view(doc, view, Align::Center, unobtrusive_statusline); } } else { editor.new_file(Action::VerticalSplit); @@ -396,10 +397,12 @@ impl Application { self.editor.refresh_config(); // reset view position in case softwrap was enabled/disabled - let scrolloff = self.editor.config().scrolloff; + let config = self.editor.config(); + let scrolloff = config.scrolloff; + let unobtrusive_statusline = config.statusline.unobtrusive; for (view, _) in self.editor.tree.views() { let doc = doc_mut!(self.editor, &view.doc); - view.ensure_cursor_in_view(doc, scrolloff); + view.ensure_cursor_in_view(doc, scrolloff, unobtrusive_statusline); } } @@ -1171,6 +1174,8 @@ impl Application { } }; + let config = &self.editor.config(); + let unobtrusive_statusline = config.statusline.unobtrusive; let doc = doc_mut!(self.editor, &doc_id); if let Some(range) = selection { // TODO: convert inside server @@ -1181,7 +1186,7 @@ impl Application { // (for example start of the function). doc.set_selection(view.id, Selection::single(new_range.head, new_range.anchor)); if action.align_view(view, doc.id()) { - align_view(doc, view, Align::Center); + align_view(doc, view, Align::Center, unobtrusive_statusline); } } else { log::warn!("lsp position out of bounds - {:?}", range); diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index ee2949fa0..db227eff1 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -676,9 +676,12 @@ type MoveFn = fn move_impl(cx: &mut Context, move_fn: MoveFn, dir: Direction, behaviour: Movement) { let count = cx.count(); + let config = cx.editor.config(); let (view, doc) = current!(cx.editor); + let unobtrusive_statusline = config.statusline.unobtrusive; + let text = doc.text().slice(..); - let text_fmt = doc.text_format(view.inner_area(doc).width, None); + let text_fmt = doc.text_format(view.inner_area(doc, unobtrusive_statusline).width, None); let mut annotations = view.text_annotations(doc, None); let selection = doc.selection(view.id).clone().transform(|range| { @@ -1079,10 +1082,11 @@ fn align_selections(cx: &mut Context) { fn goto_window(cx: &mut Context, align: Align) { let count = cx.count() - 1; let config = cx.editor.config(); + let unobtrusive_statusline = config.statusline.unobtrusive; let (view, doc) = current!(cx.editor); let view_offset = doc.view_offset(view.id); - let height = view.inner_height(); + let height = view.inner_height(unobtrusive_statusline); // respect user given count if any // - 1 so we have at least one gap in the middle. @@ -1090,7 +1094,7 @@ fn goto_window(cx: &mut Context, align: Align) { // as we type let scrolloff = config.scrolloff.min(height.saturating_sub(1) / 2); - let last_visual_line = view.last_visual_line(doc); + let last_visual_line = view.last_visual_line(doc, unobtrusive_statusline); let visual_line = match align { Align::Top => view_offset.vertical_offset + scrolloff + count, @@ -1752,7 +1756,7 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction, sync_cursor let text = doc.text().slice(..); let cursor = range.cursor(text); - let height = view.inner_height(); + let height = view.inner_height(config.statusline.unobtrusive); let scrolloff = config.scrolloff.min(height.saturating_sub(1) / 2); let offset = match direction { @@ -1761,7 +1765,7 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction, sync_cursor }; let doc_text = doc.text().slice(..); - let viewport = view.inner_area(doc); + let viewport = view.inner_area(doc, config.statusline.unobtrusive); let text_fmt = doc.text_format(viewport.width, None); (view_offset.anchor, view_offset.vertical_offset) = char_idx_at_visual_offset( doc_text, @@ -1854,49 +1858,49 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction, sync_cursor fn page_up(cx: &mut Context) { let view = view!(cx.editor); - let offset = view.inner_height(); + let offset = view.inner_height(cx.editor.config().statusline.unobtrusive); scroll(cx, offset, Direction::Backward, false); } fn page_down(cx: &mut Context) { let view = view!(cx.editor); - let offset = view.inner_height(); + let offset = view.inner_height(cx.editor.config().statusline.unobtrusive); scroll(cx, offset, Direction::Forward, false); } fn half_page_up(cx: &mut Context) { let view = view!(cx.editor); - let offset = view.inner_height() / 2; + let offset = view.inner_height(cx.editor.config().statusline.unobtrusive) / 2; scroll(cx, offset, Direction::Backward, false); } fn half_page_down(cx: &mut Context) { let view = view!(cx.editor); - let offset = view.inner_height() / 2; + let offset = view.inner_height(cx.editor.config().statusline.unobtrusive) / 2; scroll(cx, offset, Direction::Forward, false); } fn page_cursor_up(cx: &mut Context) { let view = view!(cx.editor); - let offset = view.inner_height(); + let offset = view.inner_height(cx.editor.config().statusline.unobtrusive); scroll(cx, offset, Direction::Backward, true); } fn page_cursor_down(cx: &mut Context) { let view = view!(cx.editor); - let offset = view.inner_height(); + let offset = view.inner_height(cx.editor.config().statusline.unobtrusive); scroll(cx, offset, Direction::Forward, true); } fn page_cursor_half_up(cx: &mut Context) { let view = view!(cx.editor); - let offset = view.inner_height() / 2; + let offset = view.inner_height(cx.editor.config().statusline.unobtrusive) / 2; scroll(cx, offset, Direction::Backward, true); } fn page_cursor_half_down(cx: &mut Context) { let view = view!(cx.editor); - let offset = view.inner_height() / 2; + let offset = view.inner_height(cx.editor.config().statusline.unobtrusive) / 2; scroll(cx, offset, Direction::Forward, true); } @@ -2073,6 +2077,7 @@ fn search_impl( scrolloff: usize, wrap_around: bool, show_warnings: bool, + unobtrusive_statusline: bool, ) { let (view, doc) = current!(editor); let text = doc.text().slice(..); @@ -2143,7 +2148,7 @@ fn search_impl( }; doc.set_selection(view.id, selection); - view.ensure_cursor_in_view_center(doc, scrolloff); + view.ensure_cursor_in_view_center(doc, scrolloff, unobtrusive_statusline); }; } @@ -2168,6 +2173,7 @@ fn searcher(cx: &mut Context, direction: Direction) { let reg = cx.register.unwrap_or('/'); let config = cx.editor.config(); let scrolloff = config.scrolloff; + let unobtrusive_statusline = config.statusline.unobtrusive; let wrap_around = config.search.wrap_around; let movement = if cx.editor.mode() == Mode::Select { Movement::Extend @@ -2203,6 +2209,7 @@ fn searcher(cx: &mut Context, direction: Direction) { scrolloff, wrap_around, false, + unobtrusive_statusline, ); }, ); @@ -2215,6 +2222,7 @@ fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Dir .unwrap_or(cx.editor.registers.last_search_register); let config = cx.editor.config(); let scrolloff = config.scrolloff; + let unobtrusive_statusline = config.statusline.unobtrusive; if let Some(query) = cx.editor.registers.first(register, cx.editor) { let search_config = &config.search; let case_insensitive = if search_config.smart_case { @@ -2240,6 +2248,7 @@ fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Dir scrolloff, wrap_around, true, + unobtrusive_statusline, ); } } else { @@ -2498,6 +2507,7 @@ fn global_search(cx: &mut Context) { [], config, move |cx, FileResult { path, line_num, .. }, action| { + let config = cx.editor.config(); let doc = match cx.editor.open(path, action) { Ok(id) => doc_mut!(cx.editor, &id), Err(e) => { @@ -2508,6 +2518,7 @@ fn global_search(cx: &mut Context) { }; let line_num = *line_num; + let unobtrusive_statusline = config.statusline.unobtrusive; let view = view_mut!(cx.editor); let text = doc.text(); if line_num >= text.len_lines() { @@ -2521,7 +2532,7 @@ fn global_search(cx: &mut Context) { doc.set_selection(view.id, Selection::single(start, end)); if action.align_view(view, doc.id()) { - align_view(doc, view, Align::Center); + align_view(doc, view, Align::Center, unobtrusive_statusline); } }, ) @@ -3095,7 +3106,7 @@ fn jumplist_picker(cx: &mut Context) { let (view, doc) = (view_mut!(cx.editor), doc_mut!(cx.editor, &meta.id)); doc.set_selection(view.id, meta.selection.clone()); if action.align_view(view, doc.id()) { - view.ensure_cursor_in_view_center(doc, config.scrolloff); + view.ensure_cursor_in_view_center(doc, config.scrolloff, config.statusline.unobtrusive); } }, ) @@ -3271,7 +3282,7 @@ pub fn command_palette(cx: &mut Context) { let view = view_mut!(ctx.editor, focus); let doc = doc_mut!(ctx.editor, &view.doc); - view.ensure_cursor_in_view(doc, config.scrolloff); + view.ensure_cursor_in_view(doc, config.scrolloff, config.statusline.unobtrusive); if mode != Mode::Insert { doc.append_changes_to_history(view); @@ -3397,7 +3408,9 @@ async fn make_format_callback( return; } - let scrolloff = editor.config().scrolloff; + let config = editor.config(); + let scrolloff = config.scrolloff; + let unobtrusive_statusline = config.statusline.unobtrusive; let doc = doc_mut!(editor, &doc_id); let view = view_mut!(editor, view_id); @@ -3406,7 +3419,7 @@ async fn make_format_callback( doc.apply(&format, view.id); doc.append_changes_to_history(view); doc.detect_indent_and_line_ending(); - view.ensure_cursor_in_view(doc, scrolloff); + view.ensure_cursor_in_view(doc, scrolloff, unobtrusive_statusline); } else { log::info!("discarded formatting changes because the document changed"); } @@ -4451,7 +4464,10 @@ fn replace_with_yanked_impl(editor: &mut Editor, register: char, count: usize) { return; }; let values: Vec<_> = values.map(|value| value.to_string()).collect(); - let scrolloff = editor.config().scrolloff; + + let config = editor.config(); + let scrolloff = config.scrolloff; + let unobtrusive_statusline = config.statusline.unobtrusive; let (view, doc) = current!(editor); let repeat = std::iter::repeat( @@ -4475,7 +4491,7 @@ fn replace_with_yanked_impl(editor: &mut Editor, register: char, count: usize) { doc.apply(&transaction, view.id); doc.append_changes_to_history(view); - view.ensure_cursor_in_view(doc, scrolloff); + view.ensure_cursor_in_view(doc, scrolloff, unobtrusive_statusline); } fn replace_selections_with_clipboard(cx: &mut Context) { @@ -5210,7 +5226,7 @@ fn jump_forward(cx: &mut Context) { doc.set_selection(view.id, selection); // Document we switch to might not have been opened in the view before doc.ensure_view_init(view.id); - view.ensure_cursor_in_view_center(doc, config.scrolloff); + view.ensure_cursor_in_view_center(doc, config.scrolloff, config.statusline.unobtrusive); }; } @@ -5232,7 +5248,7 @@ fn jump_backward(cx: &mut Context) { doc.set_selection(view.id, selection); // Document we switch to might not have been opened in the view before doc.ensure_view_init(view.id); - view.ensure_cursor_in_view_center(doc, config.scrolloff); + view.ensure_cursor_in_view_center(doc, config.scrolloff, config.statusline.unobtrusive); }; } @@ -5374,21 +5390,25 @@ fn insert_register(cx: &mut Context) { } fn align_view_top(cx: &mut Context) { + let unobtrusive_statusline = cx.editor.config().statusline.unobtrusive; let (view, doc) = current!(cx.editor); - align_view(doc, view, Align::Top); + align_view(doc, view, Align::Top, unobtrusive_statusline); } fn align_view_center(cx: &mut Context) { + let unobtrusive_statusline = cx.editor.config().statusline.unobtrusive; let (view, doc) = current!(cx.editor); - align_view(doc, view, Align::Center); + align_view(doc, view, Align::Center, unobtrusive_statusline); } fn align_view_bottom(cx: &mut Context) { + let unobtrusive_statusline = cx.editor.config().statusline.unobtrusive; let (view, doc) = current!(cx.editor); - align_view(doc, view, Align::Bottom); + align_view(doc, view, Align::Bottom, unobtrusive_statusline); } fn align_view_middle(cx: &mut Context) { + let config = cx.editor.config(); let (view, doc) = current!(cx.editor); let inner_width = view.inner_width(doc); let text_fmt = doc.text_format(inner_width, None); @@ -5408,9 +5428,10 @@ fn align_view_middle(cx: &mut Context) { .0; let mut offset = doc.view_offset(view.id); + let unobtrusive_statusline = config.statusline.unobtrusive; offset.horizontal_offset = pos .col - .saturating_sub((view.inner_area(doc).width as usize) / 2); + .saturating_sub((view.inner_area(doc, unobtrusive_statusline).width as usize) / 2); doc.set_view_offset(view.id, offset); } @@ -5975,7 +5996,7 @@ fn shell(cx: &mut compositor::Context, cmd: &str, behavior: &ShellBehavior) { // after replace cursor may be out of bounds, do this to // make sure cursor is in view and update scroll as well - view.ensure_cursor_in_view(doc, config.scrolloff); + view.ensure_cursor_in_view(doc, config.scrolloff, config.statusline.unobtrusive); } fn shell_prompt(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) { @@ -6282,7 +6303,9 @@ fn jump_to_label(cx: &mut Context, labels: Vec, behaviour: Movement) { fn jump_to_word(cx: &mut Context, behaviour: Movement) { // Calculate the jump candidates: ranges for any visible words with two or // more characters. - let alphabet = &cx.editor.config().jump_label_alphabet; + let config = cx.editor.config(); + let alphabet = &config.jump_label_alphabet; + let unobtrusive_statusline = config.statusline.unobtrusive; let jump_label_limit = alphabet.len() * alphabet.len(); let mut words = Vec::with_capacity(jump_label_limit); let (view, doc) = current_ref!(cx.editor); @@ -6291,7 +6314,7 @@ fn jump_to_word(cx: &mut Context, behaviour: Movement) { // This is not necessarily exact if there is virtual text like soft wrap. // It's ok though because the extra jump labels will not be rendered. let start = text.line_to_char(text.char_to_line(doc.view_offset(view.id).anchor)); - let end = text.line_to_char(view.estimate_last_doc_line(doc) + 1); + let end = text.line_to_char(view.estimate_last_doc_line(doc, unobtrusive_statusline) + 1); let primary_selection = doc.selection(view.id).primary(); let cursor = primary_selection.cursor(text); diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index fcc0333e8..ee894ddd1 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -134,6 +134,7 @@ fn jump_to_position( offset_encoding: OffsetEncoding, action: Action, ) { + let config = &editor.config(); let doc = match editor.open(path, action) { Ok(id) => doc_mut!(editor, &id), Err(err) => { @@ -154,8 +155,9 @@ fn jump_to_position( // we flip the range so that the cursor sits on the start of the symbol // (for example start of the function). doc.set_selection(view.id, Selection::single(new_range.head, new_range.anchor)); + let unobtrusive_statusline = config.statusline.unobtrusive; if action.align_view(view, doc.id()) { - align_view(doc, view, Align::Center); + align_view(doc, view, Align::Center, unobtrusive_statusline); } } @@ -1239,7 +1241,9 @@ pub fn select_references_to_symbol_under_cursor(cx: &mut Context) { } pub fn compute_inlay_hints_for_all_views(editor: &mut Editor, jobs: &mut crate::job::Jobs) { - if !editor.config().lsp.display_inlay_hints { + let config = editor.config(); + let unobtrusive_statusline = config.statusline.unobtrusive; + if !config.lsp.display_inlay_hints { return; } @@ -1248,7 +1252,7 @@ pub fn compute_inlay_hints_for_all_views(editor: &mut Editor, jobs: &mut crate:: Some(doc) => doc, None => continue, }; - if let Some(callback) = compute_inlay_hints_for_view(view, doc) { + if let Some(callback) = compute_inlay_hints_for_view(view, doc, unobtrusive_statusline) { jobs.callback(callback); } } @@ -1257,6 +1261,7 @@ pub fn compute_inlay_hints_for_all_views(editor: &mut Editor, jobs: &mut crate:: fn compute_inlay_hints_for_view( view: &View, doc: &Document, + unobtrusive_statusline: bool, ) -> Option>>>> { let view_id = view.id; let doc_id = view.doc; @@ -1272,7 +1277,7 @@ fn compute_inlay_hints_for_view( // will not show half the view with hints and half without while still being faster // than computing all the hints for the full file (which could be dozens of time // longer than the view is). - let view_height = view.inner_height(); + let view_height = view.inner_height(unobtrusive_statusline); let first_visible_line = doc_text.char_to_line(doc.view_offset(view_id).anchor.min(doc_text.len_chars())); let first_line = first_visible_line.saturating_sub(view_height); diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 68ba9bab5..e1d10feff 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -127,12 +127,13 @@ fn open(cx: &mut compositor::Context, args: &[Cow], event: PromptEvent) -> cx.jobs.callback(callback); } else { // Otherwise, just open the file + let unobtrusive_statusline = cx.editor.config().statusline.unobtrusive; let _ = cx.editor.open(&path, Action::Replace)?; let (view, doc) = current!(cx.editor); let pos = Selection::point(pos_at_coords(doc.text().slice(..), pos, true)); doc.set_selection(view.id, pos); // does not affect opening a buffer without pos - align_view(doc, view, Align::Center); + align_view(doc, view, Align::Center, unobtrusive_statusline); } } Ok(()) @@ -1277,10 +1278,12 @@ fn reload( return Ok(()); } - let scrolloff = cx.editor.config().scrolloff; + let config = cx.editor.config(); + let scrolloff = config.scrolloff; + let unobtrusive_statusline = config.statusline.unobtrusive; let (view, doc) = current!(cx.editor); doc.reload(view, &cx.editor.diff_providers).map(|_| { - view.ensure_cursor_in_view(doc, scrolloff); + view.ensure_cursor_in_view(doc, scrolloff, unobtrusive_statusline); })?; if let Some(path) = doc.path() { cx.editor @@ -1300,7 +1303,9 @@ fn reload_all( return Ok(()); } - let scrolloff = cx.editor.config().scrolloff; + let config = cx.editor.config(); + let scrolloff = config.scrolloff; + let unobtrusive_statusline = config.statusline.unobtrusive; let view_id = view!(cx.editor).id; let docs_view_ids: Vec<(DocumentId, Vec)> = cx @@ -1342,7 +1347,7 @@ fn reload_all( for view_id in view_ids { let view = view_mut!(cx.editor, view_id); if view.doc.eq(&doc_id) { - view.ensure_cursor_in_view(doc, scrolloff); + view.ensure_cursor_in_view(doc, scrolloff, unobtrusive_statusline); } } } @@ -1578,6 +1583,8 @@ fn tree_sitter_highlight_name( ) -> Option { use helix_core::syntax::HighlightEvent; + let config = &cx.editor.config(); + let unobtrusive_statusline = config.statusline.unobtrusive; let (view, doc) = current!(cx.editor); let syntax = doc.syntax()?; let text = doc.text().slice(..); @@ -1590,7 +1597,7 @@ fn tree_sitter_highlight_name( let row = text.char_to_line(doc.view_offset(view.id).anchor.min(text.len_chars())); // Saturating subs to make it inclusive zero indexing. let last_line = text.len_lines().saturating_sub(1); - let height = view.inner_area(doc).height; + let height = view.inner_area(doc, unobtrusive_statusline).height; let last_visible_line = (row + height as usize).saturating_sub(1).min(last_line); let start = text.line_to_byte(row.min(last_line)); let end = text.line_to_byte(last_visible_line + 1); @@ -1795,11 +1802,14 @@ fn tutor( fn abort_goto_line_number_preview(cx: &mut compositor::Context) { if let Some(last_selection) = cx.editor.last_selection.take() { - let scrolloff = cx.editor.config().scrolloff; + let config = cx.editor.config(); + + let scrolloff = config.scrolloff; + let unobtrusive_statusline = config.statusline.unobtrusive; let (view, doc) = current!(cx.editor); doc.set_selection(view.id, last_selection); - view.ensure_cursor_in_view(doc, scrolloff); + view.ensure_cursor_in_view(doc, scrolloff, unobtrusive_statusline); } } @@ -1812,12 +1822,14 @@ fn update_goto_line_number_preview( doc.selection(view.id).clone() }); - let scrolloff = cx.editor.config().scrolloff; + let config = cx.editor.config(); + let scrolloff = config.scrolloff; + let unobtrusive_statusline = config.statusline.unobtrusive; let line = args[0].parse::()?; goto_line_without_jumplist(cx.editor, NonZeroUsize::new(line)); let (view, doc) = current!(cx.editor); - view.ensure_cursor_in_view(doc, scrolloff); + view.ensure_cursor_in_view(doc, scrolloff, unobtrusive_statusline); Ok(()) } @@ -2062,7 +2074,9 @@ fn sort_impl( _args: &[Cow], reverse: bool, ) -> anyhow::Result<()> { - let scrolloff = cx.editor.config().scrolloff; + let config = cx.editor.config(); + let scrolloff = config.scrolloff; + let unobtrusive_statusline = config.statusline.unobtrusive; let (view, doc) = current!(cx.editor); let text = doc.text().slice(..); @@ -2088,7 +2102,7 @@ fn sort_impl( doc.apply(&transaction, view.id); doc.append_changes_to_history(view); - view.ensure_cursor_in_view(doc, scrolloff); + view.ensure_cursor_in_view(doc, scrolloff, unobtrusive_statusline); Ok(()) } @@ -2102,7 +2116,9 @@ fn reflow( return Ok(()); } - let scrolloff = cx.editor.config().scrolloff; + let config = cx.editor.config(); + let scrolloff = config.scrolloff; + let unobtrusive_statusline = config.statusline.unobtrusive; let cfg_text_width: usize = cx.editor.config().text_width; let (view, doc) = current!(cx.editor); @@ -2129,7 +2145,7 @@ fn reflow( doc.apply(&transaction, view.id); doc.append_changes_to_history(view); - view.ensure_cursor_in_view(doc, scrolloff); + view.ensure_cursor_in_view(doc, scrolloff, unobtrusive_statusline); Ok(()) } @@ -2329,7 +2345,9 @@ fn reset_diff_change( ensure!(args.is_empty(), ":reset-diff-change takes no arguments"); let editor = &mut cx.editor; - let scrolloff = editor.config().scrolloff; + let config = editor.config(); + let scrolloff = config.scrolloff; + let unobtrusive_statusline = config.statusline.unobtrusive; let (view, doc) = current!(editor); let Some(handle) = doc.diff_handle() else { @@ -2363,7 +2381,7 @@ fn reset_diff_change( drop(diff); // make borrow check happy doc.apply(&transaction, view.id); doc.append_changes_to_history(view); - view.ensure_cursor_in_view(doc, scrolloff); + view.ensure_cursor_in_view(doc, scrolloff, unobtrusive_statusline); cx.editor.set_status(format!( "Reset {changes} change{}", if changes == 1 { "" } else { "s" } @@ -2492,7 +2510,9 @@ fn read(cx: &mut compositor::Context, args: &[Cow], event: PromptEvent) -> return Ok(()); } - let scrolloff = cx.editor.config().scrolloff; + let config = cx.editor.config(); + let scrolloff = config.scrolloff; + let unobtrusive_statusline = config.statusline.unobtrusive; let (view, doc) = current!(cx.editor); ensure!(!args.is_empty(), "file name is expected"); @@ -2515,7 +2535,7 @@ fn read(cx: &mut compositor::Context, args: &[Cow], event: PromptEvent) -> let transaction = Transaction::insert(doc.text(), selection, contents); doc.apply(&transaction, view.id); doc.append_changes_to_history(view); - view.ensure_cursor_in_view(doc, scrolloff); + view.ensure_cursor_in_view(doc, scrolloff, unobtrusive_statusline); Ok(()) } diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index f7541fe25..0c17c4bd4 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -88,10 +88,11 @@ impl EditorView { surface: &mut Surface, is_focused: bool, ) { - let inner = view.inner_area(doc); let area = view.area; let theme = &editor.theme; let config = editor.config(); + let unobtrusive_statusline = config.statusline.unobtrusive; + let inner = view.inner_area(doc, unobtrusive_statusline); let view_offset = doc.view_offset(view.id); @@ -103,7 +104,7 @@ impl EditorView { } if is_focused && config.cursorcolumn { - Self::highlight_cursorcolumn(doc, view, surface, theme, inner, &text_annotations); + Self::highlight_cursorcolumn(doc, view, surface, theme, inner, &text_annotations, config.statusline.unobtrusive); } // Set DAP highlights, if needed. @@ -240,7 +241,7 @@ impl EditorView { let mut context = statusline::RenderContext::new(editor, doc, view, is_focused, &self.spinners); - statusline::render(&mut context, statusline_area, surface); + statusline::render(&mut context, statusline_area, surface, unobtrusive_statusline); } pub fn render_rulers( @@ -812,6 +813,7 @@ impl EditorView { theme: &Theme, viewport: Rect, text_annotations: &TextAnnotations, + unobtrusive_statusline: bool, ) { let text = doc.text().slice(..); @@ -826,7 +828,7 @@ impl EditorView { .or_else(|| theme.try_get_exact("ui.cursorcolumn")) .unwrap_or_else(|| theme.get("ui.cursorline.secondary")); - let inner_area = view.inner_area(doc); + let inner_area = view.inner_area(doc, unobtrusive_statusline); let selection = doc.selection(view.id); let view_offset = doc.view_offset(view.id); @@ -1122,6 +1124,7 @@ impl EditorView { row, column, ignore_virtual_text, + editor.config().statusline.unobtrusive, ) .map(|pos| (pos, view.id)) }) @@ -1193,7 +1196,7 @@ impl EditorView { MouseEventKind::Drag(MouseButton::Left) => { let (view, doc) = current!(cxt.editor); - let pos = match view.pos_at_screen_coords(doc, row, column, true) { + let pos = match view.pos_at_screen_coords(doc, row, column, true, config.statusline.unobtrusive) { Some(pos) => pos, None => return EventResult::Ignored(None), }; @@ -1340,7 +1343,7 @@ impl Component for EditorView { let config = cx.editor.config(); let mode = cx.editor.mode(); let (view, doc) = current!(cx.editor); - view.ensure_cursor_in_view(doc, config.scrolloff); + view.ensure_cursor_in_view(doc, config.scrolloff, config.statusline.unobtrusive); // Store a history state if not in insert mode. Otherwise wait till we exit insert // to include any edits to the paste in the history state. @@ -1434,7 +1437,7 @@ impl Component for EditorView { let mode = cx.editor.mode(); let (view, doc) = current!(cx.editor); - view.ensure_cursor_in_view(doc, config.scrolloff); + view.ensure_cursor_in_view(doc, config.scrolloff, config.statusline.unobtrusive); // Store a history state if not in insert mode. This also takes care of // committing changes when leaving insert mode. diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 6a3e198c1..888251732 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -131,7 +131,7 @@ pub fn raw_regex_prompt( fun(cx, regex, input, event); let (view, doc) = current!(cx.editor); - view.ensure_cursor_in_view(doc, config.scrolloff); + view.ensure_cursor_in_view(doc, config.scrolloff, config.statusline.unobtrusive); } Err(err) => { let (view, doc) = current!(cx.editor); diff --git a/helix-term/src/ui/statusline.rs b/helix-term/src/ui/statusline.rs index 7437cbd07..cb930b8cd 100644 --- a/helix-term/src/ui/statusline.rs +++ b/helix-term/src/ui/statusline.rs @@ -49,14 +49,16 @@ pub struct RenderBuffer<'a> { pub right: Spans<'a>, } -pub fn render(context: &mut RenderContext, viewport: Rect, surface: &mut Surface) { +pub fn render(context: &mut RenderContext, viewport: Rect, surface: &mut Surface, unobtrusive_statusline: bool) { let base_style = if context.focused { context.editor.theme.get("ui.statusline") } else { context.editor.theme.get("ui.statusline.inactive") }; - surface.set_style(viewport.with_height(1), base_style); + if !unobtrusive_statusline { + surface.set_style(viewport.with_height(1), base_style); + } let write_left = |context: &mut RenderContext, text, style| { append(&mut context.parts.left, text, &base_style, style) diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 26dea3a21..b739e47ff 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -464,6 +464,7 @@ pub struct SearchConfig { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case", default, deny_unknown_fields)] pub struct StatusLineConfig { + pub unobtrusive: bool, pub left: Vec, pub center: Vec, pub right: Vec, @@ -476,6 +477,7 @@ impl Default for StatusLineConfig { use StatusLineElement as E; Self { + unobtrusive: false, left: vec![ E::Mode, E::Spinner, @@ -1538,12 +1540,14 @@ impl Editor { let doc = doc_mut!(self, &view.doc); view.sync_changes(doc); view.gutters = config.gutters.clone(); - view.ensure_cursor_in_view(doc, config.scrolloff) + view.ensure_cursor_in_view(doc, config.scrolloff, config.statusline.unobtrusive) } } fn replace_document_in_view(&mut self, current_view: ViewId, doc_id: DocumentId) { - let scrolloff = self.config().scrolloff; + let config = self.config(); + let scrolloff = config.scrolloff; + let unobtrusive_statusline = config.statusline.unobtrusive; let view = self.tree.get_mut(current_view); view.doc = doc_id; @@ -1553,7 +1557,7 @@ impl Editor { view.sync_changes(doc); doc.mark_as_focused(); - view.ensure_cursor_in_view(doc, scrolloff) + view.ensure_cursor_in_view(doc, scrolloff, unobtrusive_statusline) } pub fn switch(&mut self, id: DocumentId, action: Action) { @@ -1915,7 +1919,7 @@ impl Editor { let config = self.config(); let view = self.tree.get(id); let doc = doc_mut!(self, &view.doc); - view.ensure_cursor_in_view(doc, config.scrolloff) + view.ensure_cursor_in_view(doc, config.scrolloff, config.statusline.unobtrusive) } #[inline] @@ -2005,8 +2009,9 @@ impl Editor { pub fn cursor(&self) -> (Option, CursorKind) { let config = self.config(); let (view, doc) = current_ref!(self); - if let Some(mut pos) = self.cursor_cache.get(view, doc) { - let inner = view.inner_area(doc); + let unobtrusive_statusline = config.statusline.unobtrusive; + if let Some(mut pos) = self.cursor_cache.get(view, doc, unobtrusive_statusline) { + let inner = view.inner_area(doc, unobtrusive_statusline); pos.col += inner.x as usize; pos.row += inner.y as usize; let cursorkind = config.cursor_shape.from_mode(self.mode); @@ -2205,14 +2210,14 @@ fn try_restore_indent(doc: &mut Document, view: &mut View) { pub struct CursorCache(Cell>>); impl CursorCache { - pub fn get(&self, view: &View, doc: &Document) -> Option { + pub fn get(&self, view: &View, doc: &Document, unobtrusive_statusline: bool) -> Option { if let Some(pos) = self.0.get() { return pos; } let text = doc.text().slice(..); let cursor = doc.selection(view.id).primary().cursor(text); - let res = view.screen_coords_at_pos(doc, text, cursor); + let res = view.screen_coords_at_pos(doc, text, cursor, unobtrusive_statusline); self.set(res); res } diff --git a/helix-view/src/gutter.rs b/helix-view/src/gutter.rs index 36f719f79..b48af7aeb 100644 --- a/helix-view/src/gutter.rs +++ b/helix-view/src/gutter.rs @@ -147,8 +147,10 @@ pub fn line_numbers<'doc>( ) -> GutterFn<'doc> { let text = doc.text().slice(..); let width = line_numbers_width(view, doc); + let config = editor.config(); + let unobtrusive_statusline = config.statusline.unobtrusive; - let last_line_in_view = view.estimate_last_doc_line(doc); + let last_line_in_view = view.estimate_last_doc_line(doc, unobtrusive_statusline); // 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. @@ -161,7 +163,7 @@ pub fn line_numbers<'doc>( .text() .char_to_line(doc.selection(view.id).primary().cursor(text)); - let line_number = editor.config().line_number; + let line_number = config.line_number; let mode = editor.mode; Box::new( diff --git a/helix-view/src/handlers/dap.rs b/helix-view/src/handlers/dap.rs index 81766dd59..9506f8278 100644 --- a/helix-view/src/handlers/dap.rs +++ b/helix-view/src/handlers/dap.rs @@ -68,6 +68,7 @@ pub fn jump_to_stack_frame(editor: &mut Editor, frame: &helix_dap::StackFrame) { return; } + let unobtrusive_statusline = editor.config().statusline.unobtrusive; let (view, doc) = current!(editor); let text_end = doc.text().len_chars().saturating_sub(1); @@ -79,7 +80,7 @@ pub fn jump_to_stack_frame(editor: &mut Editor, frame: &helix_dap::StackFrame) { let selection = Selection::single(start.min(text_end), end.min(text_end)); doc.set_selection(view.id, selection); - align_view(doc, view, Align::Center); + align_view(doc, view, Align::Center, unobtrusive_statusline); } pub fn breakpoints_changed( diff --git a/helix-view/src/lib.rs b/helix-view/src/lib.rs index d54b49ef5..a91ed8fdd 100644 --- a/helix-view/src/lib.rs +++ b/helix-view/src/lib.rs @@ -47,10 +47,10 @@ pub enum Align { Bottom, } -pub fn align_view(doc: &mut Document, view: &View, align: Align) { +pub fn align_view(doc: &mut Document, view: &View, align: Align, unobtrusive_statusline: bool) { let doc_text = doc.text().slice(..); let cursor = doc.selection(view.id).primary().cursor(doc_text); - let viewport = view.inner_area(doc); + let viewport = view.inner_area(doc, unobtrusive_statusline); let last_line_height = viewport.height.saturating_sub(1); let mut view_offset = doc.view_offset(view.id); diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs index a229f01ea..88d3f3e3a 100644 --- a/helix-view/src/view.rs +++ b/helix-view/src/view.rs @@ -190,12 +190,32 @@ impl View { self.docs_access_history.push(id); } - pub fn inner_area(&self, doc: &Document) -> Rect { - self.area.clip_left(self.gutter_offset(doc)).clip_bottom(1) // -1 for statusline + /// Accounts for height of statusline + pub fn inner_area(&self, doc: &Document, unobtrusive_statusline: bool) -> Rect { + if unobtrusive_statusline { + self.area.clip_left(self.gutter_offset(doc)) + } else { + self.area.clip_left(self.gutter_offset(doc)).clip_bottom(1) // -1 for statusline + } + } + + /// Accounts for height of statusline + pub fn inner_height(&self, unobtrusive_statusline: bool) -> usize { + if unobtrusive_statusline { + self.area.height.into() + } else { + self.area.clip_bottom(1).height.into() // -1 for statusline + } + } + + /// Does not account for height of statusline + pub fn inner_area_raw(&self, doc: &Document) -> Rect { + self.area.clip_left(self.gutter_offset(doc)) } - pub fn inner_height(&self) -> usize { - self.area.clip_bottom(1).height.into() // -1 for statusline + /// Does not account for height of statusline + pub fn inner_height_raw(&self) -> usize { + self.area.height.into() } pub fn inner_width(&self, doc: &Document) -> u16 { @@ -225,18 +245,20 @@ impl View { &self, doc: &Document, scrolloff: usize, + unobtrusive_statusline: bool, ) -> Option { - self.offset_coords_to_in_view_center::(doc, scrolloff) + self.offset_coords_to_in_view_center::(doc, scrolloff, unobtrusive_statusline) } pub fn offset_coords_to_in_view_center( &self, doc: &Document, scrolloff: usize, + unobtrusive_statusline: bool, ) -> Option { let view_offset = doc.get_view_offset(self.id)?; let doc_text = doc.text().slice(..); - let viewport = self.inner_area(doc); + let viewport = self.inner_area(doc, unobtrusive_statusline); let vertical_viewport_end = view_offset.vertical_offset + viewport.height as usize; let text_fmt = doc.text_format(viewport.width, None); let annotations = self.text_annotations(doc, None); @@ -333,22 +355,22 @@ impl View { Some(offset) } - pub fn ensure_cursor_in_view(&self, doc: &mut Document, scrolloff: usize) { - if let Some(offset) = self.offset_coords_to_in_view_center::(doc, scrolloff) { + pub fn ensure_cursor_in_view(&self, doc: &mut Document, scrolloff: usize, unobtrusive_statusline: bool) { + if let Some(offset) = self.offset_coords_to_in_view_center::(doc, scrolloff, unobtrusive_statusline) { doc.set_view_offset(self.id, offset); } } - pub fn ensure_cursor_in_view_center(&self, doc: &mut Document, scrolloff: usize) { - if let Some(offset) = self.offset_coords_to_in_view_center::(doc, scrolloff) { + pub fn ensure_cursor_in_view_center(&self, doc: &mut Document, scrolloff: usize, unobtrusive_statusline: bool) { + if let Some(offset) = self.offset_coords_to_in_view_center::(doc, scrolloff, unobtrusive_statusline) { doc.set_view_offset(self.id, offset); } else { - align_view(doc, self, Align::Center); + align_view(doc, self, Align::Center, unobtrusive_statusline); } } - pub fn is_cursor_in_view(&mut self, doc: &Document, scrolloff: usize) -> bool { - self.offset_coords_to_in_view(doc, scrolloff).is_none() + pub fn is_cursor_in_view(&mut self, doc: &Document, scrolloff: usize, unobtrusive_statusline: bool) -> bool { + self.offset_coords_to_in_view(doc, scrolloff, unobtrusive_statusline).is_none() } /// Estimates the last visible document line on screen. @@ -357,20 +379,20 @@ impl View { /// The actual last visible line may be smaller if softwrapping occurs /// or virtual text lines are visible #[inline] - pub fn estimate_last_doc_line(&self, doc: &Document) -> usize { + pub fn estimate_last_doc_line(&self, doc: &Document, unobtrusive_statusline: bool) -> usize { let doc_text = doc.text().slice(..); let line = doc_text.char_to_line(doc.view_offset(self.id).anchor.min(doc_text.len_chars())); // Saturating subs to make it inclusive zero indexing. - (line + self.inner_height()) + (line + self.inner_height(unobtrusive_statusline)) .min(doc_text.len_lines()) .saturating_sub(1) } /// Calculates the last non-empty visual line on screen #[inline] - pub fn last_visual_line(&self, doc: &Document) -> usize { + pub fn last_visual_line(&self, doc: &Document, unobtrusive_statusline: bool) -> usize { let doc_text = doc.text().slice(..); - let viewport = self.inner_area(doc); + let viewport = self.inner_area(doc, unobtrusive_statusline); let text_fmt = doc.text_format(viewport.width, None); let annotations = self.text_annotations(doc, None); let view_offset = doc.view_offset(self.id); @@ -379,7 +401,7 @@ impl View { let visual_height = doc.view_offset(self.id).vertical_offset + viewport.height as usize; // fast path when the EOF is not visible on the screen, - if self.estimate_last_doc_line(doc) < doc_text.len_lines() - 1 { + if self.estimate_last_doc_line(doc, unobtrusive_statusline) < doc_text.len_lines() - 1 { return visual_height.saturating_sub(1); } @@ -408,10 +430,11 @@ impl View { doc: &Document, text: RopeSlice, pos: usize, + unobtrusive_statusline: bool, ) -> Option { let view_offset = doc.view_offset(self.id); - let viewport = self.inner_area(doc); + let viewport = self.inner_area(doc, unobtrusive_statusline); let text_fmt = doc.text_format(viewport.width, None); let annotations = self.text_annotations(doc, None); @@ -512,8 +535,9 @@ impl View { fmt: TextFormat, annotations: &TextAnnotations, ignore_virtual_text: bool, + unobtrusive_statusline: bool, ) -> Option { - let inner = self.inner_area(doc); + let inner = self.inner_area(doc, unobtrusive_statusline); // 1 for status if row < inner.top() || row >= inner.bottom() { return None; @@ -572,6 +596,7 @@ impl View { row: u16, column: u16, ignore_virtual_text: bool, + unobtrusive_statusline: bool, ) -> Option { self.text_pos_at_screen_coords( doc, @@ -580,6 +605,7 @@ impl View { doc.text_format(self.inner_width(doc), None), &self.text_annotations(doc, None), ignore_virtual_text, + unobtrusive_statusline, ) } @@ -712,7 +738,8 @@ mod tests { 2, TextFormat::default(), &TextAnnotations::default(), - true + true, + false ), None ); @@ -725,6 +752,7 @@ mod tests { TextFormat::default(), &TextAnnotations::default(), true + false, ), None ); @@ -736,7 +764,8 @@ mod tests { 2, TextFormat::default(), &TextAnnotations::default(), - true + true, + false, ), None ); @@ -748,7 +777,8 @@ mod tests { 49, TextFormat::default(), &TextAnnotations::default(), - true + true, + false, ), None ); @@ -760,7 +790,8 @@ mod tests { 41, TextFormat::default(), &TextAnnotations::default(), - true + true, + false ), None ); @@ -772,7 +803,8 @@ mod tests { 81, TextFormat::default(), &TextAnnotations::default(), - true + true, + false, ), None ); @@ -784,7 +816,8 @@ mod tests { 41, TextFormat::default(), &TextAnnotations::default(), - true + true, + false ), None ); @@ -796,7 +829,8 @@ mod tests { 40 + DEFAULT_GUTTER_OFFSET + 3, TextFormat::default(), &TextAnnotations::default(), - true + true, + false, ), Some(3) ); @@ -808,7 +842,8 @@ mod tests { 80, TextFormat::default(), &TextAnnotations::default(), - true + true, + false, ), Some(3) ); @@ -820,7 +855,8 @@ mod tests { 40 + DEFAULT_GUTTER_OFFSET + 1, TextFormat::default(), &TextAnnotations::default(), - true + true, + false, ), Some(4) ); @@ -832,7 +868,8 @@ mod tests { 40 + DEFAULT_GUTTER_OFFSET + 4, TextFormat::default(), &TextAnnotations::default(), - true + true, + false, ), Some(5) ); @@ -844,7 +881,8 @@ mod tests { 40 + DEFAULT_GUTTER_OFFSET + 7, TextFormat::default(), &TextAnnotations::default(), - true + true, + false, ), Some(8) ); @@ -856,7 +894,8 @@ mod tests { 80, TextFormat::default(), &TextAnnotations::default(), - true + true, + false, ), Some(8) ); @@ -886,7 +925,8 @@ mod tests { 40 + DEFAULT_GUTTER_OFFSET_ONLY_DIAGNOSTICS + 1, TextFormat::default(), &TextAnnotations::default(), - true + true, + false, ), Some(4) ); @@ -916,7 +956,8 @@ mod tests { 40 + 1, TextFormat::default(), &TextAnnotations::default(), - true + true, + false ), Some(4) ); @@ -941,7 +982,8 @@ mod tests { 40 + DEFAULT_GUTTER_OFFSET, TextFormat::default(), &TextAnnotations::default(), - true + true, + false ), Some(0) ); @@ -953,7 +995,8 @@ mod tests { 40 + DEFAULT_GUTTER_OFFSET + 4, TextFormat::default(), &TextAnnotations::default(), - true + true, + false ), Some(4) ); @@ -964,7 +1007,8 @@ mod tests { 40 + DEFAULT_GUTTER_OFFSET + 5, TextFormat::default(), &TextAnnotations::default(), - true + true, + false ), Some(4) ); @@ -976,7 +1020,8 @@ mod tests { 40 + DEFAULT_GUTTER_OFFSET + 6, TextFormat::default(), &TextAnnotations::default(), - true + true, + false ), Some(5) ); @@ -988,7 +1033,8 @@ mod tests { 40 + DEFAULT_GUTTER_OFFSET + 7, TextFormat::default(), &TextAnnotations::default(), - true + true, + false ), Some(5) ); @@ -1000,7 +1046,8 @@ mod tests { 40 + DEFAULT_GUTTER_OFFSET + 8, TextFormat::default(), &TextAnnotations::default(), - true + true, + false ), Some(6) ); @@ -1025,7 +1072,8 @@ mod tests { 40 + DEFAULT_GUTTER_OFFSET, TextFormat::default(), &TextAnnotations::default(), - true + true, + false ), Some(0) ); @@ -1037,7 +1085,8 @@ mod tests { 40 + DEFAULT_GUTTER_OFFSET + 1, TextFormat::default(), &TextAnnotations::default(), - true + true, + false ), Some(1) ); @@ -1049,7 +1098,8 @@ mod tests { 40 + DEFAULT_GUTTER_OFFSET + 2, TextFormat::default(), &TextAnnotations::default(), - true + true, + false ), Some(3) ); @@ -1061,7 +1111,8 @@ mod tests { 40 + DEFAULT_GUTTER_OFFSET + 3, TextFormat::default(), &TextAnnotations::default(), - true + true, + false ), Some(5) ); @@ -1073,7 +1124,8 @@ mod tests { 40 + DEFAULT_GUTTER_OFFSET + 4, TextFormat::default(), &TextAnnotations::default(), - true + true, + false, ), Some(7) );