diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index b27927207..d3c5dd76b 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -110,7 +110,7 @@ fn align_view(doc: &Document, view: &mut View, align: Align) { .cursor(doc.text().slice(..)); let line = doc.text().char_to_line(pos); - let height = view.area.height.saturating_sub(1) as usize; // -1 for statusline + let height = view.inner_area().height as usize; let relative = match align { Align::Center => height / 2, @@ -455,7 +455,7 @@ fn goto_first_nonwhitespace(cx: &mut Context) { fn goto_window(cx: &mut Context, align: Align) { let (view, doc) = current!(cx.editor); - let height = view.area.height.saturating_sub(1) as usize; // -1 for statusline + let height = view.inner_area().height as usize; // - 1 so we have at least one gap in the middle. // a height of 6 with padding of 3 on each side will keep shifting the view back and forth @@ -898,11 +898,9 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) { return; } - let scrolloff = cx - .editor - .config - .scrolloff - .min(view.area.height as usize / 2); + let height = view.inner_area().height; + + let scrolloff = cx.editor.config.scrolloff.min(height as usize / 2); view.offset.row = match direction { Forward => view.offset.row + offset, @@ -928,25 +926,25 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) { fn page_up(cx: &mut Context) { let view = view!(cx.editor); - let offset = view.area.height as usize; + let offset = view.inner_area().height as usize; scroll(cx, offset, Direction::Backward); } fn page_down(cx: &mut Context) { let view = view!(cx.editor); - let offset = view.area.height as usize; + let offset = view.inner_area().height as usize; scroll(cx, offset, Direction::Forward); } fn half_page_up(cx: &mut Context) { let view = view!(cx.editor); - let offset = view.area.height as usize / 2; + let offset = view.inner_area().height as usize / 2; scroll(cx, offset, Direction::Backward); } fn half_page_down(cx: &mut Context) { let view = view!(cx.editor); - let offset = view.area.height as usize / 2; + let offset = view.inner_area().height as usize / 2; scroll(cx, offset, Direction::Forward); } @@ -4113,9 +4111,9 @@ fn align_view_middle(cx: &mut Context) { .cursor(doc.text().slice(..)); let pos = coords_at_pos(doc.text().slice(..), pos); - view.offset.col = pos.col.saturating_sub( - ((view.area.width as usize).saturating_sub(crate::ui::editor::GUTTER_OFFSET as usize)) / 2, - ); + view.offset.col = pos + .col + .saturating_sub((view.inner_area().width as usize) / 2); } fn scroll_up(cx: &mut Context) { diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 985d4e487..906577643 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -314,7 +314,7 @@ impl Component for Completion { let half = area.height / 2; let height = 15.min(half); // we want to make sure the cursor is visible (not hidden behind the documentation) - let y = if cursor_pos + view.area.y + let y = if cursor_pos + area.y >= (cx.editor.tree.area().height - height - 2/* statusline + commandline */) { 0 diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 98462e266..21e6cd9bd 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -38,8 +38,6 @@ pub struct EditorView { autoinfo: Option, } -pub const GUTTER_OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter - impl Default for EditorView { fn default() -> Self { Self::new(Keymaps::default()) @@ -74,15 +72,10 @@ impl EditorView { loader: &syntax::Loader, config: &helix_view::editor::Config, ) { - let area = Rect::new( - view.area.x + GUTTER_OFFSET, - view.area.y, - view.area.width - GUTTER_OFFSET, - view.area.height.saturating_sub(1), - ); // - 1 for statusline - let height = view.area.height.saturating_sub(1); // - 1 for statusline - - let highlights = Self::doc_syntax_highlights(doc, view.offset, height, theme, loader); + let inner = view.inner_area(); + let area = view.area; + + let highlights = Self::doc_syntax_highlights(doc, view.offset, inner.height, theme, loader); let highlights = syntax::merge(highlights, Self::doc_diagnostics_highlights(doc, theme)); let highlights: Box> = if is_focused { Box::new(syntax::merge( @@ -93,11 +86,11 @@ impl EditorView { Box::new(highlights) }; - Self::render_text_highlights(doc, view.offset, area, surface, theme, highlights); - Self::render_gutter(doc, view, area, surface, theme, config); + Self::render_text_highlights(doc, view.offset, inner, surface, theme, highlights); + Self::render_gutter(doc, view, view.area, surface, theme, is_focused, config); if is_focused { - Self::render_focused_view_elements(view, doc, area, theme, surface); + Self::render_focused_view_elements(view, doc, inner, theme, surface); } // if we're not at the edge of the screen, draw a right border @@ -113,7 +106,7 @@ impl EditorView { } } - self.render_diagnostics(doc, view, area, surface, theme); + self.render_diagnostics(doc, view, inner, surface, theme); let area = Rect::new( view.area.x, @@ -369,7 +362,7 @@ impl EditorView { } } - /// Render brace match, selected line numbers, etc (meant for the focused view only) + /// Render brace match, etc (meant for the focused view only) pub fn render_focused_view_elements( view: &View, doc: &Document, @@ -377,77 +370,29 @@ impl EditorView { theme: &Theme, surface: &mut Surface, ) { - let text = doc.text().slice(..); - let selection = doc.selection(view.id); - let last_line = view.last_line(doc); - let screen = { - let start = text.line_to_char(view.offset.row); - let end = text.line_to_char(last_line + 1) + 1; // +1 for cursor at end of text. - Range::new(start, end) - }; - - // render selected linenr(s) - let linenr_select: Style = theme - .try_get("ui.linenr.selected") - .unwrap_or_else(|| theme.get("ui.linenr")); - - // 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) < text.len_bytes(); - - for selection in selection.iter().filter(|range| range.overlaps(&screen)) { - let head = view.screen_coords_at_pos( - doc, - text, - if selection.head > selection.anchor { - selection.head - 1 - } else { - selection.head - }, - ); - if let Some(head) = head { - // Highlight line number for selected lines. - let line_number = view.offset.row + head.row; - let line_number_text = if line_number == last_line && !draw_last { - " ~".into() - } else { - format!("{:>5}", line_number + 1) - }; - surface.set_stringn( - viewport.x - GUTTER_OFFSET + 1, - viewport.y + head.row as u16, - line_number_text, - 5, - linenr_select, - ); + // Highlight matching braces + if let Some(syntax) = doc.syntax() { + let text = doc.text().slice(..); + use helix_core::match_brackets; + let pos = doc.selection(view.id).primary().cursor(text); + + let pos = match_brackets::find(syntax, doc.text(), pos) + .and_then(|pos| view.screen_coords_at_pos(doc, text, pos)); + + if let Some(pos) = pos { + // ensure col is on screen + if (pos.col as u16) < viewport.width + view.offset.col as u16 + && pos.col >= view.offset.col + { + let style = theme.try_get("ui.cursor.match").unwrap_or_else(|| { + Style::default() + .add_modifier(Modifier::REVERSED) + .add_modifier(Modifier::DIM) + }); - // Highlight matching braces - // TODO: set cursor position for IME - if let Some(syntax) = doc.syntax() { - use helix_core::match_brackets; - let pos = doc - .selection(view.id) - .primary() - .cursor(doc.text().slice(..)); - let pos = match_brackets::find(syntax, doc.text(), pos) - .and_then(|pos| view.screen_coords_at_pos(doc, text, pos)); - - if let Some(pos) = pos { - // ensure col is on screen - if (pos.col as u16) < viewport.width + view.offset.col as u16 - && pos.col >= view.offset.col - { - let style = theme.try_get("ui.cursor.match").unwrap_or_else(|| { - Style::default() - .add_modifier(Modifier::REVERSED) - .add_modifier(Modifier::DIM) - }); - - surface - .get_mut(viewport.x + pos.col as u16, viewport.y + pos.row as u16) - .set_style(style); - } - } + surface + .get_mut(viewport.x + pos.col as u16, viewport.y + pos.row as u16) + .set_style(style); } } } @@ -460,12 +405,15 @@ impl EditorView { viewport: Rect, surface: &mut Surface, theme: &Theme, + is_focused: bool, config: &helix_view::editor::Config, ) { let text = doc.text().slice(..); let last_line = view.last_line(doc); let linenr = theme.get("ui.linenr"); + let linenr_select: Style = theme.try_get("ui.linenr.selected").unwrap_or(linenr); + let warning = theme.get("warning"); let error = theme.get("error"); let info = theme.get("info"); @@ -478,11 +426,21 @@ impl EditorView { let current_line = doc .text() .char_to_line(doc.selection(view.id).primary().anchor); + + // it's used inside an iterator so the collect isn't needless: + // https://github.com/rust-lang/rust-clippy/issues/6164 + #[allow(clippy::clippy::needless_collect)] + let cursors: Vec<_> = doc + .selection(view.id) + .iter() + .map(|range| range.cursor_line(text)) + .collect(); + for (i, line) in (view.offset.row..(last_line + 1)).enumerate() { use helix_core::diagnostic::Severity; if let Some(diagnostic) = doc.diagnostics().iter().find(|d| d.line == line) { surface.set_stringn( - viewport.x - GUTTER_OFFSET, + viewport.x, viewport.y + i as u16, "●", 1, @@ -495,25 +453,27 @@ impl EditorView { ); } - // Line numbers having selections are rendered - // differently, further below. - let line_number_text = if line == last_line && !draw_last { + let selected = cursors.contains(&line); + + let text = if line == last_line && !draw_last { " ~".into() } else { - match config.line_number { - LineNumber::Absolute => format!("{:>5}", line + 1), - LineNumber::Relative => { - let relative_line = abs_diff(current_line, line); - format!("{:>5}", relative_line) - } - } + let line = match config.line_number { + LineNumber::Absolute => line + 1, + LineNumber::Relative => abs_diff(current_line, line), + }; + format!("{:>5}", line) }; surface.set_stringn( - viewport.x + 1 - GUTTER_OFFSET, + viewport.x + 1, viewport.y + i as u16, - line_number_text, + text, 5, - linenr, + if selected && is_focused { + linenr_select + } else { + linenr + }, ); } } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 478f38180..ec80580eb 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -194,7 +194,7 @@ impl Editor { .primary() .cursor(doc.text().slice(..)); let line = doc.text().char_to_line(pos); - view.offset.row = line.saturating_sub(view.area.height as usize / 2); + view.offset.row = line.saturating_sub(view.inner_area().height as usize / 2); return; } @@ -344,7 +344,6 @@ impl Editor { // } pub fn cursor(&self) -> (Option, CursorKind) { - const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter let view = view!(self); let doc = &self.documents[view.doc]; let cursor = doc @@ -352,8 +351,9 @@ impl Editor { .primary() .cursor(doc.text().slice(..)); if let Some(mut pos) = view.screen_coords_at_pos(doc, doc.text().slice(..), cursor) { - pos.col += view.area.x as usize + OFFSET as usize; - pos.row += view.area.y as usize; + let inner = view.inner_area(); + pos.col += inner.x as usize; + pos.row += inner.y as usize; (Some(pos), CursorKind::Hidden) } else { (None, CursorKind::Hidden) diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs index f688dd7f0..b707c092d 100644 --- a/helix-view/src/view.rs +++ b/helix-view/src/view.rs @@ -80,25 +80,32 @@ impl View { } } + pub fn inner_area(&self) -> Rect { + // TODO: not ideal + const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter + Rect::new( + self.area.x + OFFSET, + self.area.y, + self.area.width - OFFSET, + self.area.height.saturating_sub(1), // -1 for statusline + ) + } + pub fn ensure_cursor_in_view(&mut self, doc: &Document, scrolloff: usize) { let cursor = doc .selection(self.id) .primary() .cursor(doc.text().slice(..)); - let pos = coords_at_pos(doc.text().slice(..), cursor); - let line = pos.row; - let col = pos.col; - let height = self.area.height.saturating_sub(1); // - 1 for statusline - let last_line = (self.offset.row + height as usize).saturating_sub(1); + let Position { col, row: line } = coords_at_pos(doc.text().slice(..), cursor); + let inner_area = self.inner_area(); + let last_line = (self.offset.row + inner_area.height as usize).saturating_sub(1); // - 1 so we have at least one gap in the middle. // a height of 6 with padding of 3 on each side will keep shifting the view back and forth // as we type - let scrolloff = scrolloff.min(height.saturating_sub(1) as usize / 2); + let scrolloff = scrolloff.min(inner_area.height.saturating_sub(1) as usize / 2); - // TODO: not ideal - const OFFSET: usize = 7; // 1 diagnostic + 5 linenr + 1 gutter - let last_col = (self.offset.col + self.area.width as usize).saturating_sub(OFFSET + 1); + let last_col = self.offset.col + inner_area.width.saturating_sub(1) as usize; if line > last_line.saturating_sub(scrolloff) { // scroll down @@ -120,7 +127,7 @@ impl View { /// Calculates the last visible line on screen #[inline] pub fn last_line(&self, doc: &Document) -> usize { - let height = self.area.height.saturating_sub(1); // - 1 for statusline + let height = self.inner_area().height; std::cmp::min( // Saturating subs to make it inclusive zero indexing. (self.offset.row + height as usize).saturating_sub(1), @@ -172,19 +179,17 @@ impl View { column: u16, tab_width: usize, ) -> Option { - // TODO: not ideal - const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter - - // 2 for status - if row < self.area.top() || row > self.area.bottom().saturating_sub(2) { + let inner = self.inner_area(); + // 1 for status + if row < inner.top() || row >= inner.bottom() { return None; } - if column < self.area.left() + OFFSET || column > self.area.right() { + if column < inner.left() || column > inner.right() { return None; } - let line_number = (row - self.area.y) as usize + self.offset.row; + let line_number = (row - inner.y) as usize + self.offset.row; if line_number > text.len_lines() - 1 { return Some(text.len_chars()); @@ -194,7 +199,7 @@ impl View { let current_line = text.line(line_number); - let target = (column - OFFSET - self.area.x) as usize + self.offset.col; + let target = (column - inner.x) as usize + self.offset.col; let mut selected = 0; for grapheme in RopeGraphemes::new(current_line) {