2021-03-22 04:40:07 +01:00
|
|
|
use crate::{
|
|
|
|
commands,
|
2022-08-09 03:31:26 +02:00
|
|
|
compositor::{Component, Context, Event, EventResult},
|
2022-07-12 05:38:26 +02:00
|
|
|
job::{self, Callback},
|
|
|
|
key,
|
2022-03-20 07:55:11 +01:00
|
|
|
keymap::{KeymapResult, Keymaps},
|
2021-06-20 21:31:45 +02:00
|
|
|
ui::{Completion, ProgressSpinners},
|
2021-03-22 04:40:07 +01:00
|
|
|
};
|
2020-12-13 05:35:30 +01:00
|
|
|
|
2021-03-12 06:46:23 +01:00
|
|
|
use helix_core::{
|
2022-01-12 02:22:16 +01:00
|
|
|
graphemes::{
|
|
|
|
ensure_grapheme_boundary_next_byte, next_grapheme_boundary, prev_grapheme_boundary,
|
|
|
|
},
|
2021-08-10 07:35:20 +02:00
|
|
|
movement::Direction,
|
2021-11-07 10:12:30 +01:00
|
|
|
syntax::{self, HighlightEvent},
|
2021-07-28 06:57:07 +02:00
|
|
|
unicode::width::UnicodeWidthStr,
|
2022-10-08 20:28:42 +02:00
|
|
|
visual_coords_at_pos, LineEnding, Position, Range, Selection, Transaction,
|
2021-03-12 06:46:23 +01:00
|
|
|
};
|
2021-06-25 05:58:15 +02:00
|
|
|
use helix_view::{
|
2022-10-10 22:15:37 +02:00
|
|
|
apply_transaction,
|
2022-09-02 04:39:38 +02:00
|
|
|
document::{Mode, SCRATCH_BUFFER_NAME},
|
2022-03-01 02:45:29 +01:00
|
|
|
editor::{CompleteAction, CursorShapeConfig},
|
2022-06-21 18:35:25 +02:00
|
|
|
graphics::{Color, CursorKind, Modifier, Rect, Style},
|
2022-08-09 03:31:26 +02:00
|
|
|
input::{KeyEvent, MouseButton, MouseEvent, MouseEventKind},
|
2021-06-25 05:58:15 +02:00
|
|
|
keyboard::{KeyCode, KeyModifiers},
|
|
|
|
Document, Editor, Theme, View,
|
|
|
|
};
|
2022-10-26 04:15:46 +02:00
|
|
|
use std::{borrow::Cow, cmp::min, num::NonZeroUsize, path::PathBuf};
|
2020-12-13 05:35:30 +01:00
|
|
|
|
2021-07-01 19:41:20 +02:00
|
|
|
use tui::buffer::Buffer as Surface;
|
2020-12-13 04:23:50 +01:00
|
|
|
|
2022-07-19 04:28:24 +02:00
|
|
|
use super::lsp::SignatureHelp;
|
2022-07-18 02:57:01 +02:00
|
|
|
use super::statusline;
|
|
|
|
|
2020-12-13 04:23:50 +01:00
|
|
|
pub struct EditorView {
|
2022-02-17 06:03:11 +01:00
|
|
|
pub keymaps: Keymaps,
|
2021-03-11 02:44:38 +01:00
|
|
|
on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>,
|
2022-10-03 17:14:57 +02:00
|
|
|
pseudo_pending: Vec<KeyEvent>,
|
2022-03-01 02:45:29 +01:00
|
|
|
last_insert: (commands::MappableCommand, Vec<InsertEvent>),
|
2021-08-26 04:14:46 +02:00
|
|
|
pub(crate) completion: Option<Completion>,
|
2021-06-20 21:31:45 +02:00
|
|
|
spinners: ProgressSpinners,
|
2020-12-13 04:23:50 +01:00
|
|
|
}
|
|
|
|
|
2022-03-01 02:45:29 +01:00
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
pub enum InsertEvent {
|
|
|
|
Key(KeyEvent),
|
|
|
|
CompletionApply(CompleteAction),
|
|
|
|
TriggerCompletion,
|
|
|
|
}
|
|
|
|
|
2021-06-07 16:08:51 +02:00
|
|
|
impl Default for EditorView {
|
|
|
|
fn default() -> Self {
|
2021-06-17 13:08:05 +02:00
|
|
|
Self::new(Keymaps::default())
|
2021-06-07 16:08:51 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-13 04:23:50 +01:00
|
|
|
impl EditorView {
|
2021-06-17 13:08:05 +02:00
|
|
|
pub fn new(keymaps: Keymaps) -> Self {
|
2020-12-13 04:23:50 +01:00
|
|
|
Self {
|
2021-06-17 13:08:05 +02:00
|
|
|
keymaps,
|
2021-03-11 02:44:38 +01:00
|
|
|
on_next_key: None,
|
2022-10-03 17:14:57 +02:00
|
|
|
pseudo_pending: Vec::new(),
|
2021-12-04 15:47:18 +01:00
|
|
|
last_insert: (commands::MappableCommand::normal_mode, Vec::new()),
|
2021-04-05 11:23:37 +02:00
|
|
|
completion: None,
|
2021-06-20 21:31:45 +02:00
|
|
|
spinners: ProgressSpinners::default(),
|
2020-12-13 04:23:50 +01:00
|
|
|
}
|
|
|
|
}
|
2021-03-29 08:04:29 +02:00
|
|
|
|
2021-06-20 21:31:45 +02:00
|
|
|
pub fn spinners_mut(&mut self) -> &mut ProgressSpinners {
|
|
|
|
&mut self.spinners
|
|
|
|
}
|
|
|
|
|
2020-12-13 04:23:50 +01:00
|
|
|
pub fn render_view(
|
|
|
|
&self,
|
2021-11-30 08:47:46 +01:00
|
|
|
editor: &Editor,
|
2021-03-23 09:47:40 +01:00
|
|
|
doc: &Document,
|
2021-03-12 06:46:23 +01:00
|
|
|
view: &View,
|
2020-12-13 04:23:50 +01:00
|
|
|
viewport: Rect,
|
|
|
|
surface: &mut Surface,
|
2021-02-04 08:49:55 +01:00
|
|
|
is_focused: bool,
|
2020-12-13 04:23:50 +01:00
|
|
|
) {
|
2021-08-19 06:19:15 +02:00
|
|
|
let inner = view.inner_area();
|
|
|
|
let area = view.area;
|
2021-11-30 08:47:46 +01:00
|
|
|
let theme = &editor.theme;
|
2021-08-19 06:19:15 +02:00
|
|
|
|
2021-12-01 04:56:50 +01:00
|
|
|
// DAP: Highlight current stack frame position
|
|
|
|
let stack_frame = editor.debugger.as_ref().and_then(|debugger| {
|
|
|
|
if let (Some(frame), Some(thread_id)) = (debugger.active_frame, debugger.thread_id) {
|
|
|
|
debugger
|
|
|
|
.stack_frames
|
|
|
|
.get(&thread_id)
|
|
|
|
.and_then(|bt| bt.get(frame))
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
});
|
|
|
|
if let Some(frame) = stack_frame {
|
|
|
|
if doc.path().is_some()
|
|
|
|
&& frame
|
|
|
|
.source
|
|
|
|
.as_ref()
|
|
|
|
.and_then(|source| source.path.as_ref())
|
|
|
|
== doc.path()
|
|
|
|
{
|
|
|
|
let line = frame.line - 1; // convert to 0-indexing
|
|
|
|
if line >= view.offset.row && line < view.offset.row + area.height as usize {
|
|
|
|
surface.set_style(
|
|
|
|
Rect::new(
|
|
|
|
area.x,
|
|
|
|
area.y + (line - view.offset.row) as u16,
|
|
|
|
area.width,
|
|
|
|
1,
|
|
|
|
),
|
|
|
|
theme.get("ui.highlight"),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-27 10:51:22 +02:00
|
|
|
if is_focused && editor.config().cursorline {
|
2022-06-27 10:09:34 +02:00
|
|
|
Self::highlight_cursorline(doc, view, surface, theme);
|
|
|
|
}
|
2022-10-08 20:28:42 +02:00
|
|
|
if is_focused && editor.config().cursorcolumn {
|
|
|
|
Self::highlight_cursorcolumn(doc, view, surface, theme);
|
|
|
|
}
|
2022-06-27 10:09:34 +02:00
|
|
|
|
Overlay all diagnostics with highest severity on top (#4113)
Here we separate the diagnostics by severity and then overlay the Vec
of spans for each severity on top of the highlights. The error
diagnostics end up overlaid on the warning diagnostics, which are
overlaid on the hints, overlaid on info, overlaid on any other severity
(default), then overlaid on the syntax highlights.
This fixes two things:
* Error diagnostics are now always visible when overlapped with other
diagnostics.
* Ghost text is eliminated.
* Ghost text was caused by duplicate diagnostics at the EOF:
overlaps within the merged `Vec<(usize, Range<usize>)>` violate
assumptions in `helix_core::syntax::Merge`.
* When we push a new range, we check it against the last range and
merge the two if they overlap. This is safe because they both
have the same severity and therefore highlight.
The actual merge is skipped for any of these when they are empty, so
this is very fast in practice. For some data, I threw together an FPS
counter which renders as fast as possible and logs the renders per
second.
With no diagnostics, I see an FPS gain from this change from 868 FPS
to 878 (+1.1%) on a release build on a Rust file. On an Erlang file
with 12 error diagnostics and 6 warnings in view (233 errors and 66
warnings total), I see a decrease in average FPS from 795 to 790
(-0.6%) on a release build.
2022-10-11 08:46:47 +02:00
|
|
|
let mut highlights = Self::doc_syntax_highlights(doc, view.offset, inner.height, theme);
|
|
|
|
for diagnostic in Self::doc_diagnostics_highlights(doc, theme) {
|
|
|
|
// Most of the `diagnostic` Vecs are empty most of the time. Skipping
|
|
|
|
// a merge for any empty Vec saves a significant amount of work.
|
|
|
|
if diagnostic.is_empty() {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
highlights = Box::new(syntax::merge(highlights, diagnostic));
|
|
|
|
}
|
2021-08-12 09:00:42 +02:00
|
|
|
let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
|
|
|
|
Box::new(syntax::merge(
|
|
|
|
highlights,
|
2022-09-01 09:14:38 +02:00
|
|
|
Self::doc_selection_highlights(
|
|
|
|
editor.mode(),
|
|
|
|
doc,
|
|
|
|
view,
|
|
|
|
theme,
|
|
|
|
&editor.config().cursor_shape,
|
|
|
|
),
|
2021-08-12 09:00:42 +02:00
|
|
|
))
|
|
|
|
} else {
|
|
|
|
Box::new(highlights)
|
|
|
|
};
|
|
|
|
|
2021-12-01 23:59:23 +01:00
|
|
|
Self::render_text_highlights(
|
|
|
|
doc,
|
|
|
|
view.offset,
|
|
|
|
inner,
|
|
|
|
surface,
|
|
|
|
theme,
|
|
|
|
highlights,
|
2022-05-31 19:13:08 +02:00
|
|
|
&editor.config(),
|
2021-12-01 23:59:23 +01:00
|
|
|
);
|
2021-11-30 08:47:46 +01:00
|
|
|
Self::render_gutter(editor, doc, view, view.area, surface, theme, is_focused);
|
2022-04-20 03:44:32 +02:00
|
|
|
Self::render_rulers(editor, doc, view, inner, surface, theme);
|
2021-08-12 09:00:42 +02:00
|
|
|
|
|
|
|
if is_focused {
|
2021-08-19 06:19:15 +02:00
|
|
|
Self::render_focused_view_elements(view, doc, inner, theme, surface);
|
2021-08-12 09:00:42 +02:00
|
|
|
}
|
2020-12-22 08:48:34 +01:00
|
|
|
|
2021-04-08 09:54:55 +02:00
|
|
|
// if we're not at the edge of the screen, draw a right border
|
|
|
|
if viewport.right() != view.area.right() {
|
|
|
|
let x = area.right();
|
|
|
|
let border_style = theme.get("ui.window");
|
|
|
|
for y in area.top()..area.bottom() {
|
2022-01-16 02:55:28 +01:00
|
|
|
surface[(x, y)]
|
2021-06-29 18:01:28 +02:00
|
|
|
.set_symbol(tui::symbols::line::VERTICAL)
|
|
|
|
//.set_symbol(" ")
|
2021-04-08 09:54:55 +02:00
|
|
|
.set_style(border_style);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-30 09:52:39 +01:00
|
|
|
self.render_diagnostics(doc, view, inner, surface, theme);
|
2021-03-15 08:19:31 +01:00
|
|
|
|
2021-08-21 07:21:20 +02:00
|
|
|
let statusline_area = view
|
|
|
|
.area
|
|
|
|
.clip_top(view.area.height.saturating_sub(1))
|
|
|
|
.clip_bottom(1); // -1 from bottom to remove commandline
|
2022-07-18 02:57:01 +02:00
|
|
|
|
|
|
|
let mut context =
|
|
|
|
statusline::RenderContext::new(editor, doc, view, is_focused, &self.spinners);
|
|
|
|
|
|
|
|
statusline::render(&mut context, statusline_area, surface);
|
2020-12-13 04:23:50 +01:00
|
|
|
}
|
|
|
|
|
2022-04-20 03:44:32 +02:00
|
|
|
pub fn render_rulers(
|
|
|
|
editor: &Editor,
|
|
|
|
doc: &Document,
|
|
|
|
view: &View,
|
|
|
|
viewport: Rect,
|
|
|
|
surface: &mut Surface,
|
|
|
|
theme: &Theme,
|
|
|
|
) {
|
|
|
|
let editor_rulers = &editor.config().rulers;
|
2022-06-21 18:35:25 +02:00
|
|
|
let ruler_theme = theme
|
|
|
|
.try_get("ui.virtual.ruler")
|
|
|
|
.unwrap_or_else(|| Style::default().bg(Color::Red));
|
2022-04-20 03:44:32 +02:00
|
|
|
|
|
|
|
let rulers = doc
|
|
|
|
.language_config()
|
|
|
|
.and_then(|config| config.rulers.as_ref())
|
|
|
|
.unwrap_or(editor_rulers);
|
|
|
|
|
|
|
|
rulers
|
|
|
|
.iter()
|
|
|
|
// View might be horizontally scrolled, convert from absolute distance
|
|
|
|
// from the 1st column to relative distance from left of viewport
|
|
|
|
.filter_map(|ruler| ruler.checked_sub(1 + view.offset.col as u16))
|
|
|
|
.filter(|ruler| ruler < &viewport.width)
|
|
|
|
.map(|ruler| viewport.clip_left(ruler).with_width(1))
|
|
|
|
.for_each(|area| surface.set_style(area, ruler_theme))
|
|
|
|
}
|
|
|
|
|
2021-08-12 09:00:42 +02:00
|
|
|
/// Get syntax highlights for a document in a view represented by the first line
|
|
|
|
/// and column (`offset`) and the last line. This is done instead of using a view
|
|
|
|
/// directly to enable rendering syntax highlighted docs anywhere (eg. picker preview)
|
|
|
|
pub fn doc_syntax_highlights<'doc>(
|
|
|
|
doc: &'doc Document,
|
|
|
|
offset: Position,
|
|
|
|
height: u16,
|
2021-11-06 16:21:03 +01:00
|
|
|
_theme: &Theme,
|
2021-08-12 09:00:42 +02:00
|
|
|
) -> Box<dyn Iterator<Item = HighlightEvent> + 'doc> {
|
2021-03-16 10:27:57 +01:00
|
|
|
let text = doc.text().slice(..);
|
2021-08-12 09:00:42 +02:00
|
|
|
let last_line = std::cmp::min(
|
|
|
|
// Saturating subs to make it inclusive zero indexing.
|
|
|
|
(offset.row + height as usize).saturating_sub(1),
|
|
|
|
doc.text().len_lines().saturating_sub(1),
|
|
|
|
);
|
2020-12-13 04:23:50 +01:00
|
|
|
|
|
|
|
let range = {
|
|
|
|
// calculate viewport byte ranges
|
2021-08-12 09:00:42 +02:00
|
|
|
let start = text.line_to_byte(offset.row);
|
2021-03-12 06:46:23 +01:00
|
|
|
let end = text.line_to_byte(last_line + 1);
|
2020-12-13 04:23:50 +01:00
|
|
|
|
|
|
|
start..end
|
|
|
|
};
|
|
|
|
|
2022-01-26 03:58:52 +01:00
|
|
|
match doc.syntax() {
|
2020-12-13 04:23:50 +01:00
|
|
|
Some(syntax) => {
|
2022-01-26 03:58:52 +01:00
|
|
|
let iter = syntax
|
|
|
|
// TODO: range doesn't actually restrict source, just highlight range
|
2021-11-06 16:21:03 +01:00
|
|
|
.highlight_iter(text.slice(..), Some(range), None)
|
2021-08-12 09:00:42 +02:00
|
|
|
.map(|event| event.unwrap())
|
2022-01-26 03:58:52 +01:00
|
|
|
.map(move |event| match event {
|
2022-01-17 08:28:56 +01:00
|
|
|
// TODO: use byte slices directly
|
2022-01-26 03:58:52 +01:00
|
|
|
// convert byte offsets to char offset
|
|
|
|
HighlightEvent::Source { start, end } => {
|
|
|
|
let start =
|
|
|
|
text.byte_to_char(ensure_grapheme_boundary_next_byte(text, start));
|
|
|
|
let end =
|
|
|
|
text.byte_to_char(ensure_grapheme_boundary_next_byte(text, end));
|
|
|
|
HighlightEvent::Source { start, end }
|
|
|
|
}
|
|
|
|
event => event,
|
|
|
|
});
|
|
|
|
|
|
|
|
Box::new(iter)
|
2020-12-13 04:23:50 +01:00
|
|
|
}
|
2022-01-26 03:58:52 +01:00
|
|
|
None => Box::new(
|
|
|
|
[HighlightEvent::Source {
|
|
|
|
start: text.byte_to_char(range.start),
|
|
|
|
end: text.byte_to_char(range.end),
|
|
|
|
}]
|
|
|
|
.into_iter(),
|
|
|
|
),
|
2021-08-12 09:00:42 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Get highlight spans for document diagnostics
|
|
|
|
pub fn doc_diagnostics_highlights(
|
|
|
|
doc: &Document,
|
|
|
|
theme: &Theme,
|
Overlay all diagnostics with highest severity on top (#4113)
Here we separate the diagnostics by severity and then overlay the Vec
of spans for each severity on top of the highlights. The error
diagnostics end up overlaid on the warning diagnostics, which are
overlaid on the hints, overlaid on info, overlaid on any other severity
(default), then overlaid on the syntax highlights.
This fixes two things:
* Error diagnostics are now always visible when overlapped with other
diagnostics.
* Ghost text is eliminated.
* Ghost text was caused by duplicate diagnostics at the EOF:
overlaps within the merged `Vec<(usize, Range<usize>)>` violate
assumptions in `helix_core::syntax::Merge`.
* When we push a new range, we check it against the last range and
merge the two if they overlap. This is safe because they both
have the same severity and therefore highlight.
The actual merge is skipped for any of these when they are empty, so
this is very fast in practice. For some data, I threw together an FPS
counter which renders as fast as possible and logs the renders per
second.
With no diagnostics, I see an FPS gain from this change from 868 FPS
to 878 (+1.1%) on a release build on a Rust file. On an Erlang file
with 12 error diagnostics and 6 warnings in view (233 errors and 66
warnings total), I see a decrease in average FPS from 795 to 790
(-0.6%) on a release build.
2022-10-11 08:46:47 +02:00
|
|
|
) -> [Vec<(usize, std::ops::Range<usize>)>; 5] {
|
2022-05-20 03:30:28 +02:00
|
|
|
use helix_core::diagnostic::Severity;
|
|
|
|
let get_scope_of = |scope| {
|
|
|
|
theme
|
|
|
|
.find_scope_index(scope)
|
|
|
|
// get one of the themes below as fallback values
|
|
|
|
.or_else(|| theme.find_scope_index("diagnostic"))
|
2021-08-12 09:00:42 +02:00
|
|
|
.or_else(|| theme.find_scope_index("ui.cursor"))
|
|
|
|
.or_else(|| theme.find_scope_index("ui.selection"))
|
2021-08-22 04:15:33 +02:00
|
|
|
.expect(
|
|
|
|
"at least one of the following scopes must be defined in the theme: `diagnostic`, `ui.cursor`, or `ui.selection`",
|
2022-05-20 03:30:28 +02:00
|
|
|
)
|
|
|
|
};
|
|
|
|
|
|
|
|
// basically just queries the theme color defined in the config
|
|
|
|
let hint = get_scope_of("diagnostic.hint");
|
|
|
|
let info = get_scope_of("diagnostic.info");
|
|
|
|
let warning = get_scope_of("diagnostic.warning");
|
|
|
|
let error = get_scope_of("diagnostic.error");
|
|
|
|
let r#default = get_scope_of("diagnostic"); // this is a bit redundant but should be fine
|
2021-08-12 09:00:42 +02:00
|
|
|
|
Overlay all diagnostics with highest severity on top (#4113)
Here we separate the diagnostics by severity and then overlay the Vec
of spans for each severity on top of the highlights. The error
diagnostics end up overlaid on the warning diagnostics, which are
overlaid on the hints, overlaid on info, overlaid on any other severity
(default), then overlaid on the syntax highlights.
This fixes two things:
* Error diagnostics are now always visible when overlapped with other
diagnostics.
* Ghost text is eliminated.
* Ghost text was caused by duplicate diagnostics at the EOF:
overlaps within the merged `Vec<(usize, Range<usize>)>` violate
assumptions in `helix_core::syntax::Merge`.
* When we push a new range, we check it against the last range and
merge the two if they overlap. This is safe because they both
have the same severity and therefore highlight.
The actual merge is skipped for any of these when they are empty, so
this is very fast in practice. For some data, I threw together an FPS
counter which renders as fast as possible and logs the renders per
second.
With no diagnostics, I see an FPS gain from this change from 868 FPS
to 878 (+1.1%) on a release build on a Rust file. On an Erlang file
with 12 error diagnostics and 6 warnings in view (233 errors and 66
warnings total), I see a decrease in average FPS from 795 to 790
(-0.6%) on a release build.
2022-10-11 08:46:47 +02:00
|
|
|
let mut default_vec: Vec<(usize, std::ops::Range<usize>)> = Vec::new();
|
|
|
|
let mut info_vec = Vec::new();
|
|
|
|
let mut hint_vec = Vec::new();
|
|
|
|
let mut warning_vec = Vec::new();
|
|
|
|
let mut error_vec = Vec::new();
|
|
|
|
|
2022-10-16 21:52:04 +02:00
|
|
|
for diagnostic in doc.diagnostics() {
|
Overlay all diagnostics with highest severity on top (#4113)
Here we separate the diagnostics by severity and then overlay the Vec
of spans for each severity on top of the highlights. The error
diagnostics end up overlaid on the warning diagnostics, which are
overlaid on the hints, overlaid on info, overlaid on any other severity
(default), then overlaid on the syntax highlights.
This fixes two things:
* Error diagnostics are now always visible when overlapped with other
diagnostics.
* Ghost text is eliminated.
* Ghost text was caused by duplicate diagnostics at the EOF:
overlaps within the merged `Vec<(usize, Range<usize>)>` violate
assumptions in `helix_core::syntax::Merge`.
* When we push a new range, we check it against the last range and
merge the two if they overlap. This is safe because they both
have the same severity and therefore highlight.
The actual merge is skipped for any of these when they are empty, so
this is very fast in practice. For some data, I threw together an FPS
counter which renders as fast as possible and logs the renders per
second.
With no diagnostics, I see an FPS gain from this change from 868 FPS
to 878 (+1.1%) on a release build on a Rust file. On an Erlang file
with 12 error diagnostics and 6 warnings in view (233 errors and 66
warnings total), I see a decrease in average FPS from 795 to 790
(-0.6%) on a release build.
2022-10-11 08:46:47 +02:00
|
|
|
// Separate diagnostics into different Vecs by severity.
|
|
|
|
let (vec, scope) = match diagnostic.severity {
|
|
|
|
Some(Severity::Info) => (&mut info_vec, info),
|
|
|
|
Some(Severity::Hint) => (&mut hint_vec, hint),
|
|
|
|
Some(Severity::Warning) => (&mut warning_vec, warning),
|
|
|
|
Some(Severity::Error) => (&mut error_vec, error),
|
|
|
|
_ => (&mut default_vec, r#default),
|
|
|
|
};
|
|
|
|
|
|
|
|
// If any diagnostic overlaps ranges with the prior diagnostic,
|
|
|
|
// merge the two together. Otherwise push a new span.
|
|
|
|
match vec.last_mut() {
|
|
|
|
Some((_, range)) if diagnostic.range.start <= range.end => {
|
2022-10-16 21:52:04 +02:00
|
|
|
// This branch merges overlapping diagnostics, assuming that the current
|
|
|
|
// diagnostic starts on range.start or later. If this assertion fails,
|
|
|
|
// we will discard some part of `diagnostic`. This implies that
|
|
|
|
// `doc.diagnostics()` is not sorted by `diagnostic.range`.
|
|
|
|
debug_assert!(range.start <= diagnostic.range.start);
|
Overlay all diagnostics with highest severity on top (#4113)
Here we separate the diagnostics by severity and then overlay the Vec
of spans for each severity on top of the highlights. The error
diagnostics end up overlaid on the warning diagnostics, which are
overlaid on the hints, overlaid on info, overlaid on any other severity
(default), then overlaid on the syntax highlights.
This fixes two things:
* Error diagnostics are now always visible when overlapped with other
diagnostics.
* Ghost text is eliminated.
* Ghost text was caused by duplicate diagnostics at the EOF:
overlaps within the merged `Vec<(usize, Range<usize>)>` violate
assumptions in `helix_core::syntax::Merge`.
* When we push a new range, we check it against the last range and
merge the two if they overlap. This is safe because they both
have the same severity and therefore highlight.
The actual merge is skipped for any of these when they are empty, so
this is very fast in practice. For some data, I threw together an FPS
counter which renders as fast as possible and logs the renders per
second.
With no diagnostics, I see an FPS gain from this change from 868 FPS
to 878 (+1.1%) on a release build on a Rust file. On an Erlang file
with 12 error diagnostics and 6 warnings in view (233 errors and 66
warnings total), I see a decrease in average FPS from 795 to 790
(-0.6%) on a release build.
2022-10-11 08:46:47 +02:00
|
|
|
range.end = diagnostic.range.end.max(range.end)
|
|
|
|
}
|
|
|
|
_ => vec.push((scope, diagnostic.range.start..diagnostic.range.end)),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
[default_vec, info_vec, hint_vec, warning_vec, error_vec]
|
2021-08-12 09:00:42 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Get highlight spans for selections in a document view.
|
|
|
|
pub fn doc_selection_highlights(
|
2022-09-01 09:14:38 +02:00
|
|
|
mode: Mode,
|
2021-08-12 09:00:42 +02:00
|
|
|
doc: &Document,
|
|
|
|
view: &View,
|
|
|
|
theme: &Theme,
|
2021-12-23 07:26:52 +01:00
|
|
|
cursor_shape_config: &CursorShapeConfig,
|
2021-08-12 09:00:42 +02:00
|
|
|
) -> Vec<(usize, std::ops::Range<usize>)> {
|
|
|
|
let text = doc.text().slice(..);
|
|
|
|
let selection = doc.selection(view.id);
|
|
|
|
let primary_idx = selection.primary_index();
|
2021-06-25 06:01:08 +02:00
|
|
|
|
2021-12-23 07:26:52 +01:00
|
|
|
let cursorkind = cursor_shape_config.from_mode(mode);
|
|
|
|
let cursor_is_block = cursorkind == CursorKind::Block;
|
|
|
|
|
2021-06-25 06:01:08 +02:00
|
|
|
let selection_scope = theme
|
|
|
|
.find_scope_index("ui.selection")
|
2021-08-22 04:15:33 +02:00
|
|
|
.expect("could not find `ui.selection` scope in the theme!");
|
2021-06-25 06:01:08 +02:00
|
|
|
let base_cursor_scope = theme
|
|
|
|
.find_scope_index("ui.cursor")
|
|
|
|
.unwrap_or(selection_scope);
|
|
|
|
|
2021-12-23 07:26:52 +01:00
|
|
|
let cursor_scope = match mode {
|
2021-06-25 06:01:08 +02:00
|
|
|
Mode::Insert => theme.find_scope_index("ui.cursor.insert"),
|
|
|
|
Mode::Select => theme.find_scope_index("ui.cursor.select"),
|
|
|
|
Mode::Normal => Some(base_cursor_scope),
|
|
|
|
}
|
|
|
|
.unwrap_or(base_cursor_scope);
|
|
|
|
|
2021-08-12 09:00:42 +02:00
|
|
|
let primary_cursor_scope = theme
|
|
|
|
.find_scope_index("ui.cursor.primary")
|
|
|
|
.unwrap_or(cursor_scope);
|
|
|
|
let primary_selection_scope = theme
|
|
|
|
.find_scope_index("ui.selection.primary")
|
|
|
|
.unwrap_or(selection_scope);
|
2021-06-25 06:01:08 +02:00
|
|
|
|
2021-08-12 09:00:42 +02:00
|
|
|
let mut spans: Vec<(usize, std::ops::Range<usize>)> = Vec::new();
|
|
|
|
for (i, range) in selection.iter().enumerate() {
|
2021-12-18 03:55:40 +01:00
|
|
|
let selection_is_primary = i == primary_idx;
|
2021-12-23 07:26:52 +01:00
|
|
|
let (cursor_scope, selection_scope) = if selection_is_primary {
|
2021-08-12 09:00:42 +02:00
|
|
|
(primary_cursor_scope, primary_selection_scope)
|
|
|
|
} else {
|
|
|
|
(cursor_scope, selection_scope)
|
|
|
|
};
|
2021-06-25 06:01:08 +02:00
|
|
|
|
2021-08-12 09:00:42 +02:00
|
|
|
// Special-case: cursor at end of the rope.
|
|
|
|
if range.head == range.anchor && range.head == text.len_chars() {
|
2021-12-23 07:26:52 +01:00
|
|
|
if !selection_is_primary || cursor_is_block {
|
|
|
|
// Bar and underline cursors are drawn by the terminal
|
|
|
|
// BUG: If the editor area loses focus while having a bar or
|
|
|
|
// underline cursor (eg. when a regex prompt has focus) then
|
|
|
|
// the primary cursor will be invisible. This doesn't happen
|
|
|
|
// with block cursors since we manually draw *all* cursors.
|
2021-11-24 07:47:41 +01:00
|
|
|
spans.push((cursor_scope, range.head..range.head + 1));
|
|
|
|
}
|
2021-08-12 09:00:42 +02:00
|
|
|
continue;
|
2021-06-25 06:01:08 +02:00
|
|
|
}
|
|
|
|
|
2021-08-12 09:00:42 +02:00
|
|
|
let range = range.min_width_1(text);
|
|
|
|
if range.head > range.anchor {
|
|
|
|
// Standard case.
|
|
|
|
let cursor_start = prev_grapheme_boundary(text, range.head);
|
|
|
|
spans.push((selection_scope, range.anchor..cursor_start));
|
2021-12-23 07:26:52 +01:00
|
|
|
if !selection_is_primary || cursor_is_block {
|
2021-11-24 07:47:41 +01:00
|
|
|
spans.push((cursor_scope, cursor_start..range.head));
|
|
|
|
}
|
2021-08-12 09:00:42 +02:00
|
|
|
} else {
|
|
|
|
// Reverse case.
|
|
|
|
let cursor_end = next_grapheme_boundary(text, range.head);
|
2021-12-23 07:26:52 +01:00
|
|
|
if !selection_is_primary || cursor_is_block {
|
2021-11-24 07:47:41 +01:00
|
|
|
spans.push((cursor_scope, range.head..cursor_end));
|
|
|
|
}
|
2021-08-12 09:00:42 +02:00
|
|
|
spans.push((selection_scope, cursor_end..range.anchor));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
spans
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn render_text_highlights<H: Iterator<Item = HighlightEvent>>(
|
|
|
|
doc: &Document,
|
|
|
|
offset: Position,
|
|
|
|
viewport: Rect,
|
|
|
|
surface: &mut Surface,
|
|
|
|
theme: &Theme,
|
|
|
|
highlights: H,
|
2022-05-31 19:13:08 +02:00
|
|
|
config: &helix_view::editor::Config,
|
2021-08-12 09:00:42 +02:00
|
|
|
) {
|
2022-05-31 19:13:08 +02:00
|
|
|
let whitespace = &config.whitespace;
|
2021-12-01 23:59:23 +01:00
|
|
|
use helix_view::editor::WhitespaceRenderValue;
|
|
|
|
|
2022-01-17 08:28:56 +01:00
|
|
|
// It's slightly more efficient to produce a full RopeSlice from the Rope, then slice that a bunch
|
|
|
|
// of times than it is to always call Rope::slice/get_slice (it will internally always hit RSEnum::Light).
|
2021-08-12 09:00:42 +02:00
|
|
|
let text = doc.text().slice(..);
|
2021-06-25 06:01:08 +02:00
|
|
|
|
2022-08-21 06:54:02 +02:00
|
|
|
let characters = &whitespace.characters;
|
|
|
|
|
2021-08-12 09:00:42 +02:00
|
|
|
let mut spans = Vec::new();
|
2022-10-10 10:13:25 +02:00
|
|
|
let mut visual_x = 0usize;
|
2021-08-12 09:00:42 +02:00
|
|
|
let mut line = 0u16;
|
|
|
|
let tab_width = doc.tab_width();
|
2021-12-01 23:59:23 +01:00
|
|
|
let tab = if whitespace.render.tab() == WhitespaceRenderValue::All {
|
2022-08-21 06:54:02 +02:00
|
|
|
std::iter::once(characters.tab)
|
|
|
|
.chain(std::iter::repeat(characters.tabpad).take(tab_width - 1))
|
|
|
|
.collect()
|
2021-12-01 23:59:23 +01:00
|
|
|
} else {
|
|
|
|
" ".repeat(tab_width)
|
|
|
|
};
|
2022-08-21 06:54:02 +02:00
|
|
|
let space = characters.space.to_string();
|
|
|
|
let nbsp = characters.nbsp.to_string();
|
2021-12-01 23:59:23 +01:00
|
|
|
let newline = if whitespace.render.newline() == WhitespaceRenderValue::All {
|
2022-08-21 06:54:02 +02:00
|
|
|
characters.newline.to_string()
|
2021-12-01 23:59:23 +01:00
|
|
|
} else {
|
|
|
|
" ".to_string()
|
|
|
|
};
|
2022-05-31 19:13:08 +02:00
|
|
|
let indent_guide_char = config.indent_guides.character.to_string();
|
2021-06-26 17:14:59 +02:00
|
|
|
|
2021-08-19 08:59:08 +02:00
|
|
|
let text_style = theme.get("ui.text");
|
2021-12-01 23:59:23 +01:00
|
|
|
let whitespace_style = theme.get("ui.virtual.whitespace");
|
2021-08-19 08:59:08 +02:00
|
|
|
|
2022-04-02 10:39:13 +02:00
|
|
|
let mut is_in_indent_area = true;
|
|
|
|
let mut last_line_indent_level = 0;
|
2022-08-06 17:46:23 +02:00
|
|
|
|
|
|
|
// use whitespace style as fallback for indent-guide
|
|
|
|
let indent_guide_style = text_style.patch(
|
|
|
|
theme
|
|
|
|
.try_get("ui.virtual.indent-guide")
|
|
|
|
.unwrap_or_else(|| theme.get("ui.virtual.whitespace")),
|
|
|
|
);
|
2022-04-02 10:39:13 +02:00
|
|
|
|
|
|
|
let draw_indent_guides = |indent_level, line, surface: &mut Surface| {
|
2022-05-31 19:13:08 +02:00
|
|
|
if !config.indent_guides.render {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-10-11 09:00:41 +02:00
|
|
|
let starting_indent =
|
|
|
|
(offset.col / tab_width) + config.indent_guides.skip_levels as usize;
|
2022-07-20 10:03:19 +02:00
|
|
|
|
2022-10-17 13:23:50 +02:00
|
|
|
// Don't draw indent guides outside of view
|
|
|
|
let end_indent = min(
|
|
|
|
indent_level,
|
|
|
|
// Add tab_width - 1 to round up, since the first visible
|
|
|
|
// indent might be a bit after offset.col
|
|
|
|
offset.col + viewport.width as usize + (tab_width - 1),
|
|
|
|
) / tab_width;
|
|
|
|
|
|
|
|
for i in starting_indent..end_indent {
|
2022-10-11 10:37:57 +02:00
|
|
|
let x = (viewport.x as usize + (i * tab_width) - offset.col) as u16;
|
|
|
|
let y = viewport.y + line;
|
2022-10-17 13:23:50 +02:00
|
|
|
debug_assert!(surface.in_bounds(x, y));
|
2022-10-11 10:37:57 +02:00
|
|
|
surface.set_string(x, y, &indent_guide_char, indent_guide_style);
|
2022-04-02 10:39:13 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2020-12-13 04:23:50 +01:00
|
|
|
'outer: for event in highlights {
|
2021-06-25 06:01:08 +02:00
|
|
|
match event {
|
|
|
|
HighlightEvent::HighlightStart(span) => {
|
2020-12-13 04:23:50 +01:00
|
|
|
spans.push(span);
|
|
|
|
}
|
|
|
|
HighlightEvent::HighlightEnd => {
|
|
|
|
spans.pop();
|
|
|
|
}
|
|
|
|
HighlightEvent::Source { start, end } => {
|
2022-04-30 02:48:11 +02:00
|
|
|
let is_trailing_cursor = text.len_chars() < end;
|
|
|
|
|
2021-07-02 08:36:09 +02:00
|
|
|
// `unwrap_or_else` part is for off-the-end indices of
|
|
|
|
// the rope, to allow cursor highlighting at the end
|
|
|
|
// of the rope.
|
|
|
|
let text = text.get_slice(start..end).unwrap_or_else(|| " ".into());
|
2022-04-08 02:42:57 +02:00
|
|
|
let style = spans
|
|
|
|
.iter()
|
|
|
|
.fold(text_style, |acc, span| acc.patch(theme.highlight(span.0)));
|
2020-12-13 04:23:50 +01:00
|
|
|
|
2021-12-01 23:59:23 +01:00
|
|
|
let space = if whitespace.render.space() == WhitespaceRenderValue::All
|
2022-04-30 02:48:11 +02:00
|
|
|
&& !is_trailing_cursor
|
2021-12-01 23:59:23 +01:00
|
|
|
{
|
|
|
|
&space
|
|
|
|
} else {
|
|
|
|
" "
|
|
|
|
};
|
|
|
|
|
2022-04-30 02:48:52 +02:00
|
|
|
let nbsp = if whitespace.render.nbsp() == WhitespaceRenderValue::All
|
|
|
|
&& text.len_chars() < end
|
|
|
|
{
|
|
|
|
 
|
|
|
|
} else {
|
|
|
|
" "
|
|
|
|
};
|
|
|
|
|
2020-12-13 04:23:50 +01:00
|
|
|
use helix_core::graphemes::{grapheme_width, RopeGraphemes};
|
|
|
|
|
2021-02-18 10:34:22 +01:00
|
|
|
for grapheme in RopeGraphemes::new(text) {
|
2022-10-10 10:13:25 +02:00
|
|
|
let out_of_bounds = offset.col > (visual_x as usize)
|
|
|
|
|| (visual_x as usize) >= viewport.width as usize + offset.col;
|
2021-06-27 16:16:17 +02:00
|
|
|
|
2021-06-20 09:40:41 +02:00
|
|
|
if LineEnding::from_rope_slice(&grapheme).is_some() {
|
2021-06-25 06:01:08 +02:00
|
|
|
if !out_of_bounds {
|
|
|
|
// we still want to render an empty cell with the style
|
|
|
|
surface.set_string(
|
2022-10-10 10:13:25 +02:00
|
|
|
(viewport.x as usize + visual_x - offset.col) as u16,
|
2021-06-25 06:01:08 +02:00
|
|
|
viewport.y + line,
|
2021-12-01 23:59:23 +01:00
|
|
|
&newline,
|
|
|
|
style.patch(whitespace_style),
|
2021-06-25 06:01:08 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-08-06 17:46:23 +02:00
|
|
|
draw_indent_guides(last_line_indent_level, line, surface);
|
2022-04-02 10:39:13 +02:00
|
|
|
|
2020-12-13 04:23:50 +01:00
|
|
|
visual_x = 0;
|
|
|
|
line += 1;
|
2022-04-02 10:39:13 +02:00
|
|
|
is_in_indent_area = true;
|
2020-12-13 04:23:50 +01:00
|
|
|
|
|
|
|
// TODO: with proper iter this shouldn't be necessary
|
|
|
|
if line >= viewport.height {
|
|
|
|
break 'outer;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
let grapheme = Cow::from(grapheme);
|
2021-12-01 23:59:23 +01:00
|
|
|
let is_whitespace;
|
2020-12-13 04:23:50 +01:00
|
|
|
|
2022-05-31 19:13:08 +02:00
|
|
|
let (display_grapheme, width) = if grapheme == "\t" {
|
2021-12-01 23:59:23 +01:00
|
|
|
is_whitespace = true;
|
2021-06-27 16:16:17 +02:00
|
|
|
// make sure we display tab as appropriate amount of spaces
|
2022-03-04 03:01:33 +01:00
|
|
|
let visual_tab_width = tab_width - (visual_x as usize % tab_width);
|
2021-12-01 23:59:23 +01:00
|
|
|
let grapheme_tab_width =
|
2022-05-30 05:29:07 +02:00
|
|
|
helix_core::str_utils::char_to_byte_idx(&tab, visual_tab_width);
|
2021-12-01 23:59:23 +01:00
|
|
|
|
|
|
|
(&tab[..grapheme_tab_width], visual_tab_width)
|
|
|
|
} else if grapheme == " " {
|
|
|
|
is_whitespace = true;
|
|
|
|
(space, 1)
|
2022-04-30 02:48:52 +02:00
|
|
|
} else if grapheme == "\u{00A0}" {
|
|
|
|
is_whitespace = true;
|
|
|
|
(nbsp, 1)
|
2021-06-27 16:16:17 +02:00
|
|
|
} else {
|
2021-12-01 23:59:23 +01:00
|
|
|
is_whitespace = false;
|
2021-06-27 16:16:17 +02:00
|
|
|
// Cow will prevent allocations if span contained in a single slice
|
|
|
|
// which should really be the majority case
|
|
|
|
let width = grapheme_width(&grapheme);
|
|
|
|
(grapheme.as_ref(), width)
|
|
|
|
};
|
|
|
|
|
2022-08-06 17:46:50 +02:00
|
|
|
let cut_off_start = offset.col.saturating_sub(visual_x as usize);
|
|
|
|
|
2021-06-27 16:16:17 +02:00
|
|
|
if !out_of_bounds {
|
2021-04-09 11:48:56 +02:00
|
|
|
// if we're offscreen just keep going until we hit a new line
|
2021-06-27 16:16:17 +02:00
|
|
|
surface.set_string(
|
2022-10-10 10:13:25 +02:00
|
|
|
(viewport.x as usize + visual_x - offset.col) as u16,
|
2021-06-27 16:16:17 +02:00
|
|
|
viewport.y + line,
|
2022-05-31 19:13:08 +02:00
|
|
|
display_grapheme,
|
2021-12-01 23:59:23 +01:00
|
|
|
if is_whitespace {
|
|
|
|
style.patch(whitespace_style)
|
|
|
|
} else {
|
|
|
|
style
|
|
|
|
},
|
2021-06-27 16:16:17 +02:00
|
|
|
);
|
2022-08-06 17:46:50 +02:00
|
|
|
} else if cut_off_start != 0 && cut_off_start < width {
|
|
|
|
// partially on screen
|
|
|
|
let rect = Rect::new(
|
|
|
|
viewport.x as u16,
|
|
|
|
viewport.y + line,
|
|
|
|
(width - cut_off_start) as u16,
|
|
|
|
1,
|
|
|
|
);
|
|
|
|
surface.set_style(
|
|
|
|
rect,
|
|
|
|
if is_whitespace {
|
|
|
|
style.patch(whitespace_style)
|
|
|
|
} else {
|
|
|
|
style
|
|
|
|
},
|
|
|
|
);
|
2021-04-09 11:48:56 +02:00
|
|
|
}
|
2022-08-06 17:46:50 +02:00
|
|
|
|
2022-04-02 10:39:13 +02:00
|
|
|
if is_in_indent_area && !(grapheme == " " || grapheme == "\t") {
|
|
|
|
draw_indent_guides(visual_x, line, surface);
|
|
|
|
is_in_indent_area = false;
|
|
|
|
last_line_indent_level = visual_x;
|
|
|
|
}
|
2021-04-09 11:48:56 +02:00
|
|
|
|
2022-10-10 10:13:25 +02:00
|
|
|
visual_x = visual_x.saturating_add(width);
|
2020-12-13 04:23:50 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-08-12 09:00:42 +02:00
|
|
|
}
|
|
|
|
|
2021-08-19 06:19:15 +02:00
|
|
|
/// Render brace match, etc (meant for the focused view only)
|
2021-08-12 09:00:42 +02:00
|
|
|
pub fn render_focused_view_elements(
|
|
|
|
view: &View,
|
|
|
|
doc: &Document,
|
|
|
|
viewport: Rect,
|
|
|
|
theme: &Theme,
|
|
|
|
surface: &mut Surface,
|
|
|
|
) {
|
2021-08-19 06:19:15 +02:00
|
|
|
// 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);
|
|
|
|
|
2021-11-20 15:17:25 +01:00
|
|
|
let pos = match_brackets::find_matching_bracket(syntax, doc.text(), pos)
|
2021-08-19 06:19:15 +02:00
|
|
|
.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)
|
|
|
|
});
|
2021-08-12 09:00:42 +02:00
|
|
|
|
2022-01-16 02:55:28 +01:00
|
|
|
surface[(viewport.x + pos.col as u16, viewport.y + pos.row as u16)]
|
2021-08-19 06:19:15 +02:00
|
|
|
.set_style(style);
|
2021-08-12 09:00:42 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-12-13 04:23:50 +01:00
|
|
|
|
2022-09-02 04:39:38 +02:00
|
|
|
/// Render bufferline at the top
|
|
|
|
pub fn render_bufferline(editor: &Editor, viewport: Rect, surface: &mut Surface) {
|
|
|
|
let scratch = PathBuf::from(SCRATCH_BUFFER_NAME); // default filename to use for scratch buffer
|
|
|
|
surface.clear_with(
|
|
|
|
viewport,
|
|
|
|
editor
|
|
|
|
.theme
|
|
|
|
.try_get("ui.bufferline.background")
|
|
|
|
.unwrap_or_else(|| editor.theme.get("ui.statusline")),
|
|
|
|
);
|
|
|
|
|
|
|
|
let bufferline_active = editor
|
|
|
|
.theme
|
|
|
|
.try_get("ui.bufferline.active")
|
|
|
|
.unwrap_or_else(|| editor.theme.get("ui.statusline.active"));
|
|
|
|
|
|
|
|
let bufferline_inactive = editor
|
|
|
|
.theme
|
|
|
|
.try_get("ui.bufferline")
|
|
|
|
.unwrap_or_else(|| editor.theme.get("ui.statusline.inactive"));
|
|
|
|
|
|
|
|
let mut x = viewport.x;
|
|
|
|
let current_doc = view!(editor).doc;
|
|
|
|
|
|
|
|
for doc in editor.documents() {
|
|
|
|
let fname = doc
|
|
|
|
.path()
|
|
|
|
.unwrap_or(&scratch)
|
|
|
|
.file_name()
|
|
|
|
.unwrap_or_default()
|
|
|
|
.to_str()
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
|
|
let style = if current_doc == doc.id() {
|
|
|
|
bufferline_active
|
|
|
|
} else {
|
|
|
|
bufferline_inactive
|
|
|
|
};
|
|
|
|
|
|
|
|
let text = format!(" {}{} ", fname, if doc.is_modified() { "[+]" } else { "" });
|
|
|
|
let used_width = viewport.x.saturating_sub(x);
|
|
|
|
let rem_width = surface.area.width.saturating_sub(used_width);
|
|
|
|
|
|
|
|
x = surface
|
|
|
|
.set_stringn(x, viewport.y, text, rem_width as usize, style)
|
|
|
|
.0;
|
|
|
|
|
|
|
|
if x >= surface.area.right() {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-12 09:00:42 +02:00
|
|
|
pub fn render_gutter(
|
2021-11-30 08:47:46 +01:00
|
|
|
editor: &Editor,
|
2021-08-12 09:00:42 +02:00
|
|
|
doc: &Document,
|
|
|
|
view: &View,
|
|
|
|
viewport: Rect,
|
|
|
|
surface: &mut Surface,
|
|
|
|
theme: &Theme,
|
2021-08-19 06:19:15 +02:00
|
|
|
is_focused: bool,
|
2021-08-12 09:00:42 +02:00
|
|
|
) {
|
|
|
|
let text = doc.text().slice(..);
|
|
|
|
let last_line = view.last_line(doc);
|
2021-06-14 16:01:17 +02:00
|
|
|
|
2021-08-19 06:19:15 +02:00
|
|
|
// it's used inside an iterator so the collect isn't needless:
|
|
|
|
// https://github.com/rust-lang/rust-clippy/issues/6164
|
2021-08-19 09:05:05 +02:00
|
|
|
#[allow(clippy::needless_collect)]
|
2021-08-19 06:19:15 +02:00
|
|
|
let cursors: Vec<_> = doc
|
|
|
|
.selection(view.id)
|
|
|
|
.iter()
|
|
|
|
.map(|range| range.cursor_line(text))
|
|
|
|
.collect();
|
|
|
|
|
2021-09-24 03:29:41 +02:00
|
|
|
let mut offset = 0;
|
2021-11-22 09:02:46 +01:00
|
|
|
|
2021-12-13 16:52:15 +01:00
|
|
|
let gutter_style = theme.get("ui.gutter");
|
2022-10-03 16:44:45 +02:00
|
|
|
let gutter_selected_style = theme.get("ui.gutter.selected");
|
2021-12-13 16:52:15 +01:00
|
|
|
|
2021-11-22 09:02:46 +01:00
|
|
|
// avoid lots of small allocations by reusing a text buffer for each line
|
|
|
|
let mut text = String::with_capacity(8);
|
|
|
|
|
2022-07-18 03:13:47 +02:00
|
|
|
for (constructor, width) in view.gutters() {
|
2021-11-30 08:47:46 +01:00
|
|
|
let gutter = constructor(editor, doc, view, theme, is_focused, *width);
|
2021-11-22 09:02:46 +01:00
|
|
|
text.reserve(*width); // ensure there's enough space for the gutter
|
2021-09-24 03:29:41 +02:00
|
|
|
for (i, line) in (view.offset.row..(last_line + 1)).enumerate() {
|
|
|
|
let selected = cursors.contains(&line);
|
2022-04-12 09:48:30 +02:00
|
|
|
let x = viewport.x + offset;
|
|
|
|
let y = viewport.y + i as u16;
|
2021-09-24 03:29:41 +02:00
|
|
|
|
2022-10-03 16:44:45 +02:00
|
|
|
let gutter_style = if selected {
|
|
|
|
gutter_selected_style
|
|
|
|
} else {
|
|
|
|
gutter_style
|
|
|
|
};
|
|
|
|
|
2021-11-22 09:02:46 +01:00
|
|
|
if let Some(style) = gutter(line, selected, &mut text) {
|
2022-04-12 09:48:30 +02:00
|
|
|
surface.set_stringn(x, y, &text, *width, gutter_style.patch(style));
|
|
|
|
} else {
|
|
|
|
surface.set_style(
|
|
|
|
Rect {
|
|
|
|
x,
|
|
|
|
y,
|
|
|
|
width: *width as u16,
|
|
|
|
height: 1,
|
|
|
|
},
|
|
|
|
gutter_style,
|
2021-08-30 04:22:26 +02:00
|
|
|
);
|
2021-08-21 13:15:29 +02:00
|
|
|
}
|
2021-11-22 09:02:46 +01:00
|
|
|
text.clear();
|
2021-08-19 04:24:53 +02:00
|
|
|
}
|
|
|
|
|
2021-09-24 03:29:41 +02:00
|
|
|
offset += *width as u16;
|
2021-06-14 16:01:17 +02:00
|
|
|
}
|
2020-12-13 04:23:50 +01:00
|
|
|
}
|
|
|
|
|
2021-03-15 08:19:31 +01:00
|
|
|
pub fn render_diagnostics(
|
|
|
|
&self,
|
|
|
|
doc: &Document,
|
2021-03-31 10:17:01 +02:00
|
|
|
view: &View,
|
2021-03-15 08:19:31 +01:00
|
|
|
viewport: Rect,
|
|
|
|
surface: &mut Surface,
|
|
|
|
theme: &Theme,
|
|
|
|
) {
|
|
|
|
use helix_core::diagnostic::Severity;
|
|
|
|
use tui::{
|
|
|
|
layout::Alignment,
|
|
|
|
text::Text,
|
2021-11-16 07:02:48 +01:00
|
|
|
widgets::{Paragraph, Widget, Wrap},
|
2021-03-15 08:19:31 +01:00
|
|
|
};
|
|
|
|
|
2021-07-26 17:40:30 +02:00
|
|
|
let cursor = doc
|
|
|
|
.selection(view.id)
|
|
|
|
.primary()
|
|
|
|
.cursor(doc.text().slice(..));
|
2021-03-15 08:19:31 +01:00
|
|
|
|
2021-06-06 11:59:32 +02:00
|
|
|
let diagnostics = doc.diagnostics().iter().filter(|diagnostic| {
|
2021-03-15 08:19:31 +01:00
|
|
|
diagnostic.range.start <= cursor && diagnostic.range.end >= cursor
|
|
|
|
});
|
|
|
|
|
2021-08-12 09:00:42 +02:00
|
|
|
let warning = theme.get("warning");
|
|
|
|
let error = theme.get("error");
|
|
|
|
let info = theme.get("info");
|
|
|
|
let hint = theme.get("hint");
|
2021-03-15 08:19:31 +01:00
|
|
|
|
|
|
|
let mut lines = Vec::new();
|
2022-06-28 16:59:10 +02:00
|
|
|
let background_style = theme.get("ui.background");
|
2021-03-15 08:19:31 +01:00
|
|
|
for diagnostic in diagnostics {
|
2022-06-28 16:59:10 +02:00
|
|
|
let style = Style::reset()
|
|
|
|
.patch(background_style)
|
|
|
|
.patch(match diagnostic.severity {
|
|
|
|
Some(Severity::Error) => error,
|
|
|
|
Some(Severity::Warning) | None => warning,
|
|
|
|
Some(Severity::Info) => info,
|
|
|
|
Some(Severity::Hint) => hint,
|
|
|
|
});
|
2022-06-22 20:53:36 +02:00
|
|
|
let text = Text::styled(&diagnostic.message, style);
|
2021-03-15 08:19:31 +01:00
|
|
|
lines.extend(text.lines);
|
|
|
|
}
|
|
|
|
|
2021-11-16 07:02:48 +01:00
|
|
|
let paragraph = Paragraph::new(lines)
|
|
|
|
.alignment(Alignment::Right)
|
|
|
|
.wrap(Wrap { trim: true });
|
|
|
|
let width = 100.min(viewport.width);
|
2021-03-15 08:25:04 +01:00
|
|
|
let height = 15.min(viewport.height);
|
2021-03-15 08:19:31 +01:00
|
|
|
paragraph.render(
|
2021-08-21 07:21:20 +02:00
|
|
|
Rect::new(viewport.right() - width, viewport.y + 1, width, height),
|
2021-03-15 08:19:31 +01:00
|
|
|
surface,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-06-27 10:09:34 +02:00
|
|
|
/// Apply the highlighting on the lines where a cursor is active
|
|
|
|
pub fn highlight_cursorline(doc: &Document, view: &View, surface: &mut Surface, theme: &Theme) {
|
|
|
|
let text = doc.text().slice(..);
|
|
|
|
let last_line = view.last_line(doc);
|
|
|
|
|
|
|
|
let primary_line = doc.selection(view.id).primary().cursor_line(text);
|
|
|
|
|
|
|
|
// The secondary_lines do contain the primary_line, it doesn't matter
|
|
|
|
// as the else-if clause in the loop later won't test for the
|
|
|
|
// secondary_lines if primary_line == line.
|
|
|
|
// It's used inside a loop so the collect isn't needless:
|
|
|
|
// https://github.com/rust-lang/rust-clippy/issues/6164
|
|
|
|
#[allow(clippy::needless_collect)]
|
|
|
|
let secondary_lines: Vec<_> = doc
|
|
|
|
.selection(view.id)
|
|
|
|
.iter()
|
|
|
|
.map(|range| range.cursor_line(text))
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
let primary_style = theme.get("ui.cursorline.primary");
|
|
|
|
let secondary_style = theme.get("ui.cursorline.secondary");
|
|
|
|
|
|
|
|
for line in view.offset.row..(last_line + 1) {
|
|
|
|
let area = Rect::new(
|
|
|
|
view.area.x,
|
|
|
|
view.area.y + (line - view.offset.row) as u16,
|
|
|
|
view.area.width,
|
|
|
|
1,
|
|
|
|
);
|
|
|
|
if primary_line == line {
|
|
|
|
surface.set_style(area, primary_style);
|
|
|
|
} else if secondary_lines.binary_search(&line).is_ok() {
|
|
|
|
surface.set_style(area, secondary_style);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-08 20:28:42 +02:00
|
|
|
/// Apply the highlighting on the columns where a cursor is active
|
|
|
|
pub fn highlight_cursorcolumn(
|
|
|
|
doc: &Document,
|
|
|
|
view: &View,
|
|
|
|
surface: &mut Surface,
|
|
|
|
theme: &Theme,
|
|
|
|
) {
|
|
|
|
let text = doc.text().slice(..);
|
|
|
|
|
|
|
|
// Manual fallback behaviour:
|
|
|
|
// ui.cursorcolumn.{p/s} -> ui.cursorcolumn -> ui.cursorline.{p/s}
|
|
|
|
let primary_style = theme
|
|
|
|
.try_get_exact("ui.cursorcolumn.primary")
|
|
|
|
.or_else(|| theme.try_get_exact("ui.cursorcolumn"))
|
|
|
|
.unwrap_or_else(|| theme.get("ui.cursorline.primary"));
|
|
|
|
let secondary_style = theme
|
|
|
|
.try_get_exact("ui.cursorcolumn.secondary")
|
|
|
|
.or_else(|| theme.try_get_exact("ui.cursorcolumn"))
|
|
|
|
.unwrap_or_else(|| theme.get("ui.cursorline.secondary"));
|
|
|
|
|
|
|
|
let inner_area = view.inner_area();
|
|
|
|
let offset = view.offset.col;
|
|
|
|
|
|
|
|
let selection = doc.selection(view.id);
|
|
|
|
let primary = selection.primary();
|
|
|
|
for range in selection.iter() {
|
|
|
|
let is_primary = primary == *range;
|
|
|
|
|
|
|
|
let Position { row: _, col } =
|
|
|
|
visual_coords_at_pos(text, range.cursor(text), doc.tab_width());
|
|
|
|
// if the cursor is horizontally in the view
|
|
|
|
if col >= offset && inner_area.width > (col - offset) as u16 {
|
|
|
|
let area = Rect::new(
|
|
|
|
inner_area.x + (col - offset) as u16,
|
|
|
|
view.area.y,
|
|
|
|
1,
|
|
|
|
view.area.height,
|
|
|
|
);
|
|
|
|
if is_primary {
|
|
|
|
surface.set_style(area, primary_style)
|
|
|
|
} else {
|
|
|
|
surface.set_style(area, secondary_style)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-26 18:07:13 +02:00
|
|
|
/// Handle events by looking them up in `self.keymaps`. Returns None
|
|
|
|
/// if event was handled (a command was executed or a subkeymap was
|
2022-03-20 07:55:11 +01:00
|
|
|
/// activated). Only KeymapResult::{NotFound, Cancelled} is returned
|
2021-07-26 18:07:13 +02:00
|
|
|
/// otherwise.
|
|
|
|
fn handle_keymap_event(
|
|
|
|
&mut self,
|
|
|
|
mode: Mode,
|
|
|
|
cxt: &mut commands::Context,
|
|
|
|
event: KeyEvent,
|
|
|
|
) -> Option<KeymapResult> {
|
2022-08-31 03:44:06 +02:00
|
|
|
let mut last_mode = mode;
|
2022-10-03 17:14:57 +02:00
|
|
|
self.pseudo_pending.extend(self.keymaps.pending());
|
2022-03-20 07:50:48 +01:00
|
|
|
let key_result = self.keymaps.get(mode, event);
|
2022-03-20 07:55:11 +01:00
|
|
|
cxt.editor.autoinfo = self.keymaps.sticky().map(|node| node.infobox());
|
2021-09-05 05:55:13 +02:00
|
|
|
|
2022-08-31 03:44:06 +02:00
|
|
|
let mut execute_command = |command: &commands::MappableCommand| {
|
|
|
|
command.execute(cxt);
|
2022-09-01 09:14:38 +02:00
|
|
|
let current_mode = cxt.editor.mode();
|
2022-08-31 03:44:06 +02:00
|
|
|
match (last_mode, current_mode) {
|
|
|
|
(Mode::Normal, Mode::Insert) => {
|
|
|
|
// HAXX: if we just entered insert mode from normal, clear key buf
|
|
|
|
// and record the command that got us into this mode.
|
|
|
|
|
|
|
|
// how we entered insert mode is important, and we should track that so
|
|
|
|
// we can repeat the side effect.
|
|
|
|
self.last_insert.0 = command.clone();
|
|
|
|
self.last_insert.1.clear();
|
|
|
|
|
|
|
|
commands::signature_help_impl(cxt, commands::SignatureHelpInvoked::Automatic);
|
|
|
|
}
|
|
|
|
(Mode::Insert, Mode::Normal) => {
|
|
|
|
// if exiting insert mode, remove completion
|
|
|
|
self.completion = None;
|
|
|
|
|
|
|
|
// TODO: Use an on_mode_change hook to remove signature help
|
|
|
|
cxt.jobs.callback(async {
|
2022-09-03 05:38:38 +02:00
|
|
|
let call: job::Callback =
|
|
|
|
Callback::EditorCompositor(Box::new(|_editor, compositor| {
|
|
|
|
compositor.remove(SignatureHelp::ID);
|
|
|
|
}));
|
2022-08-31 03:44:06 +02:00
|
|
|
Ok(call)
|
|
|
|
});
|
|
|
|
}
|
|
|
|
_ => (),
|
|
|
|
}
|
|
|
|
last_mode = current_mode;
|
|
|
|
};
|
|
|
|
|
2022-03-20 07:55:11 +01:00
|
|
|
match &key_result {
|
2022-08-31 03:44:06 +02:00
|
|
|
KeymapResult::Matched(command) => {
|
|
|
|
execute_command(command);
|
|
|
|
}
|
2022-03-20 07:55:11 +01:00
|
|
|
KeymapResult::Pending(node) => cxt.editor.autoinfo = Some(node.infobox()),
|
|
|
|
KeymapResult::MatchedSequence(commands) => {
|
2021-11-11 05:44:50 +01:00
|
|
|
for command in commands {
|
2022-08-31 03:44:06 +02:00
|
|
|
execute_command(command);
|
2021-11-11 05:44:50 +01:00
|
|
|
}
|
|
|
|
}
|
2022-03-20 07:55:11 +01:00
|
|
|
KeymapResult::NotFound | KeymapResult::Cancelled(_) => return Some(key_result),
|
2021-03-30 11:19:27 +02:00
|
|
|
}
|
2021-07-26 18:07:13 +02:00
|
|
|
None
|
2021-03-30 11:19:27 +02:00
|
|
|
}
|
|
|
|
|
2021-07-26 18:07:13 +02:00
|
|
|
fn insert_mode(&mut self, cx: &mut commands::Context, event: KeyEvent) {
|
|
|
|
if let Some(keyresult) = self.handle_keymap_event(Mode::Insert, cx, event) {
|
2022-03-20 07:55:11 +01:00
|
|
|
match keyresult {
|
|
|
|
KeymapResult::NotFound => {
|
2021-07-26 18:07:13 +02:00
|
|
|
if let Some(ch) = event.char() {
|
|
|
|
commands::insert::insert_char(cx, ch)
|
|
|
|
}
|
|
|
|
}
|
2022-03-20 07:55:11 +01:00
|
|
|
KeymapResult::Cancelled(pending) => {
|
2021-07-26 18:07:13 +02:00
|
|
|
for ev in pending {
|
|
|
|
match ev.char() {
|
|
|
|
Some(ch) => commands::insert::insert_char(cx, ch),
|
|
|
|
None => {
|
2022-03-20 07:55:11 +01:00
|
|
|
if let KeymapResult::Matched(command) =
|
|
|
|
self.keymaps.get(Mode::Insert, ev)
|
2021-07-26 18:07:13 +02:00
|
|
|
{
|
|
|
|
command.execute(cx);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_ => unreachable!(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn command_mode(&mut self, mode: Mode, cxt: &mut commands::Context, event: KeyEvent) {
|
2022-04-20 03:50:13 +02:00
|
|
|
match (event, cxt.editor.count) {
|
2021-03-30 11:19:27 +02:00
|
|
|
// count handling
|
2022-04-20 03:50:13 +02:00
|
|
|
(key!(i @ '0'), Some(_)) | (key!(i @ '1'..='9'), _) => {
|
2021-03-30 11:19:27 +02:00
|
|
|
let i = i.to_digit(10).unwrap() as usize;
|
2021-06-08 05:24:27 +02:00
|
|
|
cxt.editor.count =
|
|
|
|
std::num::NonZeroUsize::new(cxt.editor.count.map_or(i, |c| c.get() * 10 + i));
|
2021-03-30 11:19:27 +02:00
|
|
|
}
|
|
|
|
// special handling for repeat operator
|
2022-04-20 03:50:13 +02:00
|
|
|
(key!('.'), _) if self.keymaps.pending().is_empty() => {
|
2022-10-26 04:15:46 +02:00
|
|
|
for _ in 0..cxt.editor.count.map_or(1, NonZeroUsize::into) {
|
|
|
|
// first execute whatever put us into insert mode
|
|
|
|
self.last_insert.0.execute(cxt);
|
|
|
|
// then replay the inputs
|
|
|
|
for key in self.last_insert.1.clone() {
|
|
|
|
match key {
|
|
|
|
InsertEvent::Key(key) => self.insert_mode(cxt, key),
|
|
|
|
InsertEvent::CompletionApply(compl) => {
|
|
|
|
let (view, doc) = current!(cxt.editor);
|
|
|
|
|
|
|
|
doc.restore(view);
|
|
|
|
|
|
|
|
let text = doc.text().slice(..);
|
|
|
|
let cursor = doc.selection(view.id).primary().cursor(text);
|
|
|
|
|
|
|
|
let shift_position =
|
|
|
|
|pos: usize| -> usize { pos + cursor - compl.trigger_offset };
|
|
|
|
|
|
|
|
let tx = Transaction::change(
|
|
|
|
doc.text(),
|
|
|
|
compl.changes.iter().cloned().map(|(start, end, t)| {
|
|
|
|
(shift_position(start), shift_position(end), t)
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
apply_transaction(&tx, doc, view);
|
|
|
|
}
|
|
|
|
InsertEvent::TriggerCompletion => {
|
|
|
|
let (_, doc) = current!(cxt.editor);
|
|
|
|
doc.savepoint();
|
|
|
|
}
|
2022-03-01 02:45:29 +01:00
|
|
|
}
|
|
|
|
}
|
2021-03-30 11:19:27 +02:00
|
|
|
}
|
2022-10-26 04:15:46 +02:00
|
|
|
cxt.editor.count = None;
|
2021-03-30 11:19:27 +02:00
|
|
|
}
|
|
|
|
_ => {
|
|
|
|
// set the count
|
2021-07-28 06:57:07 +02:00
|
|
|
cxt.count = cxt.editor.count;
|
2021-03-30 11:19:27 +02:00
|
|
|
// TODO: edge case: 0j -> reset to 1
|
|
|
|
// if this fails, count was Some(0)
|
|
|
|
// debug_assert!(cxt.count != 0);
|
|
|
|
|
2021-06-05 04:21:31 +02:00
|
|
|
// set the register
|
2021-09-08 07:52:09 +02:00
|
|
|
cxt.register = cxt.editor.selected_register.take();
|
2021-06-05 04:21:31 +02:00
|
|
|
|
2021-07-26 18:07:13 +02:00
|
|
|
self.handle_keymap_event(mode, cxt, event);
|
2021-07-28 06:57:07 +02:00
|
|
|
if self.keymaps.pending().is_empty() {
|
|
|
|
cxt.editor.count = None
|
|
|
|
}
|
2021-03-30 11:19:27 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-04-05 11:23:37 +02:00
|
|
|
|
|
|
|
pub fn set_completion(
|
|
|
|
&mut self,
|
2021-10-25 04:03:18 +02:00
|
|
|
editor: &mut Editor,
|
2021-04-05 11:23:37 +02:00
|
|
|
items: Vec<helix_lsp::lsp::CompletionItem>,
|
2021-04-14 08:30:15 +02:00
|
|
|
offset_encoding: helix_lsp::OffsetEncoding,
|
2021-08-27 03:44:12 +02:00
|
|
|
start_offset: usize,
|
2021-04-05 11:23:37 +02:00
|
|
|
trigger_offset: usize,
|
|
|
|
size: Rect,
|
|
|
|
) {
|
2021-08-27 03:44:12 +02:00
|
|
|
let mut completion =
|
|
|
|
Completion::new(editor, items, offset_encoding, start_offset, trigger_offset);
|
2021-10-07 03:37:35 +02:00
|
|
|
|
|
|
|
if completion.is_empty() {
|
|
|
|
// skip if we got no completion results
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-10-25 04:03:18 +02:00
|
|
|
// Immediately initialize a savepoint
|
|
|
|
doc_mut!(editor).savepoint();
|
|
|
|
|
2022-03-01 02:45:29 +01:00
|
|
|
editor.last_completion = None;
|
|
|
|
self.last_insert.1.push(InsertEvent::TriggerCompletion);
|
|
|
|
|
2021-04-05 11:23:37 +02:00
|
|
|
// TODO : propagate required size on resize to completion too
|
|
|
|
completion.required_size((size.width, size.height));
|
|
|
|
self.completion = Some(completion);
|
|
|
|
}
|
2021-10-29 09:48:25 +02:00
|
|
|
|
|
|
|
pub fn clear_completion(&mut self, editor: &mut Editor) {
|
|
|
|
self.completion = None;
|
2022-01-03 02:46:57 +01:00
|
|
|
|
2021-10-29 09:48:25 +02:00
|
|
|
// Clear any savepoints
|
2022-01-03 02:46:57 +01:00
|
|
|
let doc = doc_mut!(editor);
|
2021-10-29 09:48:25 +02:00
|
|
|
doc.savepoint = None;
|
|
|
|
editor.clear_idle_timer(); // don't retrigger
|
|
|
|
}
|
2022-03-02 08:10:35 +01:00
|
|
|
|
2022-10-11 02:10:01 +02:00
|
|
|
pub fn handle_idle_timeout(&mut self, cx: &mut commands::Context) -> EventResult {
|
2022-10-22 03:04:50 +02:00
|
|
|
if let Some(completion) = &mut self.completion {
|
|
|
|
return if completion.ensure_item_resolved(cx) {
|
|
|
|
EventResult::Consumed(None)
|
|
|
|
} else {
|
|
|
|
EventResult::Ignored(None)
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
if cx.editor.mode != Mode::Insert || !cx.editor.config().auto_completion {
|
2022-03-02 08:10:35 +01:00
|
|
|
return EventResult::Ignored(None);
|
|
|
|
}
|
|
|
|
|
2022-10-11 02:10:01 +02:00
|
|
|
crate::commands::insert::idle_completion(cx);
|
2022-03-02 08:10:35 +01:00
|
|
|
|
|
|
|
EventResult::Consumed(None)
|
|
|
|
}
|
2020-12-13 04:23:50 +01:00
|
|
|
}
|
|
|
|
|
2021-08-10 07:35:20 +02:00
|
|
|
impl EditorView {
|
|
|
|
fn handle_mouse_event(
|
|
|
|
&mut self,
|
2022-08-29 02:48:49 +02:00
|
|
|
event: &MouseEvent,
|
2021-08-10 07:35:20 +02:00
|
|
|
cxt: &mut commands::Context,
|
|
|
|
) -> EventResult {
|
2022-03-25 10:05:20 +01:00
|
|
|
let config = cxt.editor.config();
|
2022-06-27 18:05:07 +02:00
|
|
|
let MouseEvent {
|
|
|
|
kind,
|
|
|
|
row,
|
|
|
|
column,
|
|
|
|
modifiers,
|
|
|
|
..
|
2022-08-29 02:48:49 +02:00
|
|
|
} = *event;
|
2022-06-27 18:05:07 +02:00
|
|
|
|
|
|
|
let pos_and_view = |editor: &Editor, row, column| {
|
|
|
|
editor.tree.views().find_map(|(view, _focus)| {
|
|
|
|
view.pos_at_screen_coords(&editor.documents[&view.doc], row, column)
|
|
|
|
.map(|pos| (pos, view.id))
|
|
|
|
})
|
|
|
|
};
|
2021-08-10 07:35:20 +02:00
|
|
|
|
2022-06-27 18:05:07 +02:00
|
|
|
let gutter_coords_and_view = |editor: &Editor, row, column| {
|
|
|
|
editor.tree.views().find_map(|(view, _focus)| {
|
|
|
|
view.gutter_coords_at_screen_coords(row, column)
|
|
|
|
.map(|coords| (coords, view.id))
|
|
|
|
})
|
|
|
|
};
|
|
|
|
|
|
|
|
match kind {
|
|
|
|
MouseEventKind::Down(MouseButton::Left) => {
|
|
|
|
let editor = &mut cxt.editor;
|
2021-08-10 07:35:20 +02:00
|
|
|
|
2022-06-27 18:05:07 +02:00
|
|
|
if let Some((pos, view_id)) = pos_and_view(editor, row, column) {
|
2022-09-03 05:36:06 +02:00
|
|
|
let doc = doc_mut!(editor, &view!(editor, view_id).doc);
|
2021-08-10 07:35:20 +02:00
|
|
|
|
2022-08-09 03:31:26 +02:00
|
|
|
if modifiers == KeyModifiers::ALT {
|
2021-08-10 07:35:20 +02:00
|
|
|
let selection = doc.selection(view_id).clone();
|
|
|
|
doc.set_selection(view_id, selection.push(Range::point(pos)));
|
|
|
|
} else {
|
|
|
|
doc.set_selection(view_id, Selection::point(pos));
|
|
|
|
}
|
|
|
|
|
2022-08-23 07:07:50 +02:00
|
|
|
editor.focus(view_id);
|
2021-08-10 07:35:20 +02:00
|
|
|
|
|
|
|
return EventResult::Consumed(None);
|
|
|
|
}
|
|
|
|
|
2022-06-27 18:05:07 +02:00
|
|
|
if let Some((coords, view_id)) = gutter_coords_and_view(editor, row, column) {
|
2022-08-23 07:07:50 +02:00
|
|
|
editor.focus(view_id);
|
2021-09-04 20:14:24 +02:00
|
|
|
|
2022-08-23 07:07:50 +02:00
|
|
|
let (view, doc) = current!(cxt.editor);
|
2021-11-22 08:30:35 +01:00
|
|
|
|
|
|
|
let path = match doc.path() {
|
|
|
|
Some(path) => path.clone(),
|
2022-02-23 04:46:12 +01:00
|
|
|
None => return EventResult::Ignored(None),
|
2021-11-22 08:30:35 +01:00
|
|
|
};
|
2021-09-04 20:14:24 +02:00
|
|
|
|
2021-11-22 08:30:35 +01:00
|
|
|
let line = coords.row + view.offset.row;
|
|
|
|
if line < doc.text().len_lines() {
|
|
|
|
commands::dap_toggle_breakpoint_impl(cxt, path, line);
|
2021-09-04 20:14:24 +02:00
|
|
|
return EventResult::Consumed(None);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-23 04:46:12 +01:00
|
|
|
EventResult::Ignored(None)
|
2021-08-10 07:35:20 +02:00
|
|
|
}
|
|
|
|
|
2022-06-27 18:05:07 +02:00
|
|
|
MouseEventKind::Drag(MouseButton::Left) => {
|
2021-08-10 07:35:20 +02:00
|
|
|
let (view, doc) = current!(cxt.editor);
|
|
|
|
|
|
|
|
let pos = match view.pos_at_screen_coords(doc, row, column) {
|
|
|
|
Some(pos) => pos,
|
2022-02-23 04:46:12 +01:00
|
|
|
None => return EventResult::Ignored(None),
|
2021-08-10 07:35:20 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
let mut selection = doc.selection(view.id).clone();
|
|
|
|
let primary = selection.primary_mut();
|
2022-06-20 20:41:34 +02:00
|
|
|
*primary = primary.put_cursor(doc.text().slice(..), pos, true);
|
2021-08-10 07:35:20 +02:00
|
|
|
doc.set_selection(view.id, selection);
|
2022-06-27 18:05:07 +02:00
|
|
|
|
2021-08-10 07:35:20 +02:00
|
|
|
EventResult::Consumed(None)
|
|
|
|
}
|
|
|
|
|
2022-06-27 18:05:07 +02:00
|
|
|
MouseEventKind::ScrollUp | MouseEventKind::ScrollDown => {
|
2021-08-10 07:35:20 +02:00
|
|
|
let current_view = cxt.editor.tree.focus;
|
|
|
|
|
|
|
|
let direction = match event.kind {
|
|
|
|
MouseEventKind::ScrollUp => Direction::Backward,
|
|
|
|
MouseEventKind::ScrollDown => Direction::Forward,
|
|
|
|
_ => unreachable!(),
|
|
|
|
};
|
|
|
|
|
2022-06-27 18:05:07 +02:00
|
|
|
match pos_and_view(cxt.editor, row, column) {
|
|
|
|
Some((_, view_id)) => cxt.editor.tree.focus = view_id,
|
2022-02-23 04:46:12 +01:00
|
|
|
None => return EventResult::Ignored(None),
|
2021-08-10 07:35:20 +02:00
|
|
|
}
|
|
|
|
|
2022-08-04 07:32:59 +02:00
|
|
|
let offset = config.scroll_lines.unsigned_abs();
|
2021-08-10 07:35:20 +02:00
|
|
|
commands::scroll(cxt, offset, direction);
|
|
|
|
|
|
|
|
cxt.editor.tree.focus = current_view;
|
|
|
|
|
|
|
|
EventResult::Consumed(None)
|
|
|
|
}
|
2021-08-12 04:53:48 +02:00
|
|
|
|
2022-06-27 18:05:07 +02:00
|
|
|
MouseEventKind::Up(MouseButton::Left) => {
|
2022-03-25 10:05:20 +01:00
|
|
|
if !config.middle_click_paste {
|
2022-02-23 04:46:12 +01:00
|
|
|
return EventResult::Ignored(None);
|
2021-08-12 04:53:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
let (view, doc) = current!(cxt.editor);
|
|
|
|
|
2022-06-24 15:58:04 +02:00
|
|
|
if doc
|
|
|
|
.selection(view.id)
|
|
|
|
.primary()
|
2022-08-04 07:44:43 +02:00
|
|
|
.slice(doc.text().slice(..))
|
|
|
|
.len_chars()
|
2022-06-24 15:58:04 +02:00
|
|
|
<= 1
|
|
|
|
{
|
2022-02-23 04:46:12 +01:00
|
|
|
return EventResult::Ignored(None);
|
2021-08-12 04:53:48 +02:00
|
|
|
}
|
|
|
|
|
2021-12-04 15:47:18 +01:00
|
|
|
commands::MappableCommand::yank_main_selection_to_primary_clipboard.execute(cxt);
|
2021-08-12 04:53:48 +02:00
|
|
|
|
|
|
|
EventResult::Consumed(None)
|
|
|
|
}
|
|
|
|
|
2022-06-27 18:05:07 +02:00
|
|
|
MouseEventKind::Up(MouseButton::Right) => {
|
|
|
|
if let Some((coords, view_id)) = gutter_coords_and_view(cxt.editor, row, column) {
|
2022-08-23 07:07:50 +02:00
|
|
|
cxt.editor.focus(view_id);
|
2021-09-05 07:14:17 +02:00
|
|
|
|
2022-08-23 07:07:50 +02:00
|
|
|
let (view, doc) = current!(cxt.editor);
|
2021-11-22 08:30:35 +01:00
|
|
|
let line = coords.row + view.offset.row;
|
|
|
|
if let Ok(pos) = doc.text().try_line_to_char(line) {
|
2021-09-05 07:14:17 +02:00
|
|
|
doc.set_selection(view_id, Selection::point(pos));
|
2022-08-09 03:31:26 +02:00
|
|
|
if modifiers == KeyModifiers::ALT {
|
2022-02-13 10:31:51 +01:00
|
|
|
commands::MappableCommand::dap_edit_log.execute(cxt);
|
2021-09-05 07:50:03 +02:00
|
|
|
} else {
|
2022-02-13 10:31:51 +01:00
|
|
|
commands::MappableCommand::dap_edit_condition.execute(cxt);
|
2021-09-05 07:50:03 +02:00
|
|
|
}
|
2021-09-05 07:14:17 +02:00
|
|
|
|
|
|
|
return EventResult::Consumed(None);
|
|
|
|
}
|
|
|
|
}
|
2022-06-27 18:05:07 +02:00
|
|
|
|
2022-02-23 04:46:12 +01:00
|
|
|
EventResult::Ignored(None)
|
2021-09-05 07:14:17 +02:00
|
|
|
}
|
|
|
|
|
2022-06-27 18:05:07 +02:00
|
|
|
MouseEventKind::Up(MouseButton::Middle) => {
|
2021-08-12 04:53:48 +02:00
|
|
|
let editor = &mut cxt.editor;
|
2022-03-25 10:05:20 +01:00
|
|
|
if !config.middle_click_paste {
|
2022-02-23 04:46:12 +01:00
|
|
|
return EventResult::Ignored(None);
|
2021-08-12 04:53:48 +02:00
|
|
|
}
|
|
|
|
|
2022-08-09 03:31:26 +02:00
|
|
|
if modifiers == KeyModifiers::ALT {
|
2021-12-04 15:47:18 +01:00
|
|
|
commands::MappableCommand::replace_selections_with_primary_clipboard
|
|
|
|
.execute(cxt);
|
2021-08-12 04:53:48 +02:00
|
|
|
|
|
|
|
return EventResult::Consumed(None);
|
|
|
|
}
|
|
|
|
|
2022-06-27 18:05:07 +02:00
|
|
|
if let Some((pos, view_id)) = pos_and_view(editor, row, column) {
|
2022-09-03 05:36:06 +02:00
|
|
|
let doc = doc_mut!(editor, &view!(editor, view_id).doc);
|
2021-08-12 04:53:48 +02:00
|
|
|
doc.set_selection(view_id, Selection::point(pos));
|
2022-08-23 07:07:50 +02:00
|
|
|
cxt.editor.focus(view_id);
|
2021-12-04 15:47:18 +01:00
|
|
|
commands::MappableCommand::paste_primary_clipboard_before.execute(cxt);
|
2022-06-27 18:05:07 +02:00
|
|
|
|
2021-08-12 04:53:48 +02:00
|
|
|
return EventResult::Consumed(None);
|
|
|
|
}
|
|
|
|
|
2022-02-23 04:46:12 +01:00
|
|
|
EventResult::Ignored(None)
|
2021-08-12 04:53:48 +02:00
|
|
|
}
|
|
|
|
|
2022-02-23 04:46:12 +01:00
|
|
|
_ => EventResult::Ignored(None),
|
2021-08-10 07:35:20 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-13 04:23:50 +01:00
|
|
|
impl Component for EditorView {
|
2022-01-03 02:46:57 +01:00
|
|
|
fn handle_event(
|
|
|
|
&mut self,
|
2022-08-29 02:48:49 +02:00
|
|
|
event: &Event,
|
2022-01-03 02:46:57 +01:00
|
|
|
context: &mut crate::compositor::Context,
|
|
|
|
) -> EventResult {
|
|
|
|
let mut cx = commands::Context {
|
|
|
|
editor: context.editor,
|
2021-08-10 07:35:20 +02:00
|
|
|
count: None,
|
2021-09-08 07:52:09 +02:00
|
|
|
register: None,
|
2021-08-10 07:35:20 +02:00
|
|
|
callback: None,
|
|
|
|
on_next_key_callback: None,
|
2022-01-03 02:46:57 +01:00
|
|
|
jobs: context.jobs,
|
2021-08-10 07:35:20 +02:00
|
|
|
};
|
|
|
|
|
2020-12-13 04:23:50 +01:00
|
|
|
match event {
|
2022-08-29 02:48:49 +02:00
|
|
|
Event::Paste(contents) => {
|
|
|
|
cx.count = cx.editor.count;
|
|
|
|
commands::paste_bracketed_value(&mut cx, contents.clone());
|
|
|
|
cx.editor.count = None;
|
|
|
|
|
|
|
|
let config = cx.editor.config();
|
2022-09-01 09:14:38 +02:00
|
|
|
let mode = cx.editor.mode();
|
2022-08-29 02:48:49 +02:00
|
|
|
let (view, doc) = current!(cx.editor);
|
|
|
|
view.ensure_cursor_in_view(doc, config.scrolloff);
|
|
|
|
|
|
|
|
// 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.
|
2022-09-01 09:14:38 +02:00
|
|
|
if mode != Mode::Insert {
|
2022-08-29 02:48:49 +02:00
|
|
|
doc.append_changes_to_history(view.id);
|
|
|
|
}
|
|
|
|
|
|
|
|
EventResult::Consumed(None)
|
|
|
|
}
|
2021-08-10 03:58:37 +02:00
|
|
|
Event::Resize(_width, _height) => {
|
2021-06-09 08:46:13 +02:00
|
|
|
// Ignore this event, we handle resizing just before rendering to screen.
|
|
|
|
// Handling it here but not re-rendering will cause flashing
|
2020-12-13 04:23:50 +01:00
|
|
|
EventResult::Consumed(None)
|
|
|
|
}
|
2022-08-09 03:31:26 +02:00
|
|
|
Event::Key(mut key) => {
|
2022-01-03 02:46:57 +01:00
|
|
|
cx.editor.reset_idle_timer();
|
2021-06-03 15:26:24 +02:00
|
|
|
canonicalize_key(&mut key);
|
2022-01-03 02:46:57 +01:00
|
|
|
|
2021-05-07 07:19:58 +02:00
|
|
|
// clear status
|
2022-01-03 02:46:57 +01:00
|
|
|
cx.editor.status_msg = None;
|
2021-05-07 07:19:58 +02:00
|
|
|
|
2022-09-01 09:14:38 +02:00
|
|
|
let mode = cx.editor.mode();
|
|
|
|
let (view, _) = current!(cx.editor);
|
2022-08-31 03:34:04 +02:00
|
|
|
let focus = view.id;
|
2021-01-21 08:55:46 +01:00
|
|
|
|
2021-03-11 02:44:38 +01:00
|
|
|
if let Some(on_next_key) = self.on_next_key.take() {
|
|
|
|
// if there's a command waiting input, do that first
|
2022-01-03 02:46:57 +01:00
|
|
|
on_next_key(&mut cx, key);
|
2021-03-11 02:44:38 +01:00
|
|
|
} else {
|
|
|
|
match mode {
|
|
|
|
Mode::Insert => {
|
2021-04-06 08:34:33 +02:00
|
|
|
// let completion swallow the event if necessary
|
|
|
|
let mut consumed = false;
|
2021-04-05 11:23:37 +02:00
|
|
|
if let Some(completion) = &mut self.completion {
|
2021-04-06 08:34:33 +02:00
|
|
|
// use a fake context here
|
|
|
|
let mut cx = Context {
|
2022-01-03 02:46:57 +01:00
|
|
|
editor: cx.editor,
|
|
|
|
jobs: cx.jobs,
|
2021-04-06 08:34:33 +02:00
|
|
|
scroll: None,
|
|
|
|
};
|
|
|
|
let res = completion.handle_event(event, &mut cx);
|
|
|
|
|
|
|
|
if let EventResult::Consumed(callback) = res {
|
|
|
|
consumed = true;
|
|
|
|
|
|
|
|
if callback.is_some() {
|
|
|
|
// assume close_fn
|
2022-01-03 02:46:57 +01:00
|
|
|
self.clear_completion(cx.editor);
|
2021-04-06 08:34:33 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// if completion didn't take the event, we pass it onto commands
|
|
|
|
if !consumed {
|
2022-03-01 02:45:29 +01:00
|
|
|
if let Some(compl) = cx.editor.last_completion.take() {
|
|
|
|
self.last_insert.1.push(InsertEvent::CompletionApply(compl));
|
|
|
|
}
|
|
|
|
|
2022-01-03 02:46:57 +01:00
|
|
|
self.insert_mode(&mut cx, key);
|
2021-04-06 08:34:33 +02:00
|
|
|
|
2022-03-01 02:45:29 +01:00
|
|
|
// record last_insert key
|
|
|
|
self.last_insert.1.push(InsertEvent::Key(key));
|
|
|
|
|
2021-04-06 08:34:33 +02:00
|
|
|
// lastly we recalculate completion
|
|
|
|
if let Some(completion) = &mut self.completion {
|
2022-01-03 02:46:57 +01:00
|
|
|
completion.update(&mut cx);
|
2021-04-06 08:34:33 +02:00
|
|
|
if completion.is_empty() {
|
2022-01-03 02:46:57 +01:00
|
|
|
self.clear_completion(cx.editor);
|
2021-04-06 08:34:33 +02:00
|
|
|
}
|
2021-04-05 11:23:37 +02:00
|
|
|
}
|
|
|
|
}
|
2020-12-13 04:23:50 +01:00
|
|
|
}
|
2022-01-03 02:46:57 +01:00
|
|
|
mode => self.command_mode(mode, &mut cx, key),
|
2020-12-13 04:23:50 +01:00
|
|
|
}
|
2021-01-21 08:55:46 +01:00
|
|
|
}
|
2021-05-03 10:46:14 +02:00
|
|
|
|
2022-01-03 02:46:57 +01:00
|
|
|
self.on_next_key = cx.on_next_key_callback.take();
|
2022-10-03 17:14:57 +02:00
|
|
|
match self.on_next_key {
|
|
|
|
Some(_) => self.pseudo_pending.push(key),
|
|
|
|
None => self.pseudo_pending.clear(),
|
|
|
|
}
|
|
|
|
|
2021-01-21 08:55:46 +01:00
|
|
|
// appease borrowck
|
2022-01-03 02:46:57 +01:00
|
|
|
let callback = cx.callback.take();
|
2021-03-30 11:19:27 +02:00
|
|
|
|
2021-05-03 10:46:14 +02:00
|
|
|
// if the command consumed the last view, skip the render.
|
|
|
|
// on the next loop cycle the Application will then terminate.
|
2022-01-03 02:46:57 +01:00
|
|
|
if cx.editor.should_close() {
|
2022-02-23 04:46:12 +01:00
|
|
|
return EventResult::Ignored(None);
|
2021-05-03 10:46:14 +02:00
|
|
|
}
|
2022-09-07 09:42:33 +02:00
|
|
|
|
2022-09-01 18:59:39 +02:00
|
|
|
// if the focused view still exists and wasn't closed
|
|
|
|
if cx.editor.tree.contains(focus) {
|
|
|
|
let config = cx.editor.config();
|
|
|
|
let mode = cx.editor.mode();
|
2022-09-03 05:36:06 +02:00
|
|
|
let view = view_mut!(cx.editor, focus);
|
|
|
|
let doc = doc_mut!(cx.editor, &view.doc);
|
2022-09-01 18:59:39 +02:00
|
|
|
|
|
|
|
view.ensure_cursor_in_view(doc, config.scrolloff);
|
|
|
|
|
|
|
|
// Store a history state if not in insert mode. This also takes care of
|
|
|
|
// committing changes when leaving insert mode.
|
|
|
|
if mode != Mode::Insert {
|
|
|
|
doc.append_changes_to_history(view.id);
|
|
|
|
}
|
2022-01-25 08:49:53 +01:00
|
|
|
}
|
2020-12-13 04:23:50 +01:00
|
|
|
|
2021-01-21 08:55:46 +01:00
|
|
|
EventResult::Consumed(callback)
|
2020-12-13 04:23:50 +01:00
|
|
|
}
|
2021-07-30 09:52:00 +02:00
|
|
|
|
2022-01-03 02:46:57 +01:00
|
|
|
Event::Mouse(event) => self.handle_mouse_event(event, &mut cx),
|
2022-10-11 02:10:01 +02:00
|
|
|
Event::IdleTimeout => self.handle_idle_timeout(&mut cx),
|
2022-10-21 03:35:02 +02:00
|
|
|
Event::FocusGained => EventResult::Ignored(None),
|
|
|
|
Event::FocusLost => {
|
|
|
|
if context.editor.config().auto_save {
|
|
|
|
if let Err(e) = commands::typed::write_all_impl(context, false, false) {
|
|
|
|
context.editor.set_error(format!("{}", e));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
EventResult::Consumed(None)
|
|
|
|
}
|
2020-12-13 04:23:50 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-12 09:00:42 +02:00
|
|
|
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
2021-04-09 17:22:14 +02:00
|
|
|
// clear with background color
|
|
|
|
surface.set_style(area, cx.editor.theme.get("ui.background"));
|
2022-03-25 10:05:20 +01:00
|
|
|
let config = cx.editor.config();
|
2022-09-02 04:39:38 +02:00
|
|
|
|
|
|
|
// check if bufferline should be rendered
|
|
|
|
use helix_view::editor::BufferLine;
|
|
|
|
let use_bufferline = match config.bufferline {
|
|
|
|
BufferLine::Always => true,
|
|
|
|
BufferLine::Multiple if cx.editor.documents.len() > 1 => true,
|
|
|
|
_ => false,
|
|
|
|
};
|
|
|
|
|
|
|
|
// -1 for commandline and -1 for bufferline
|
|
|
|
let mut editor_area = area.clip_bottom(1);
|
|
|
|
if use_bufferline {
|
|
|
|
editor_area = editor_area.clip_top(1);
|
|
|
|
}
|
|
|
|
|
2021-06-03 03:28:49 +02:00
|
|
|
// if the terminal size suddenly changed, we need to trigger a resize
|
2022-09-02 04:39:38 +02:00
|
|
|
cx.editor.resize(editor_area);
|
|
|
|
|
|
|
|
if use_bufferline {
|
|
|
|
Self::render_bufferline(cx.editor, area.with_height(1), surface);
|
|
|
|
}
|
2021-06-03 03:28:49 +02:00
|
|
|
|
2021-02-04 08:49:55 +01:00
|
|
|
for (view, is_focused) in cx.editor.tree.views() {
|
2021-03-23 09:47:40 +01:00
|
|
|
let doc = cx.editor.document(view.doc).unwrap();
|
2021-11-30 17:07:25 +01:00
|
|
|
self.render_view(cx.editor, doc, view, area, surface, is_focused);
|
2021-02-03 11:36:54 +01:00
|
|
|
}
|
2021-04-05 11:23:37 +02:00
|
|
|
|
2022-03-25 10:05:20 +01:00
|
|
|
if config.auto_info {
|
2021-11-04 19:33:31 +01:00
|
|
|
if let Some(mut info) = cx.editor.autoinfo.take() {
|
2021-11-05 03:25:08 +01:00
|
|
|
info.render(area, surface, cx);
|
2021-11-04 19:33:31 +01:00
|
|
|
cx.editor.autoinfo = Some(info)
|
2021-11-05 03:25:08 +01:00
|
|
|
}
|
2021-06-19 17:54:37 +02:00
|
|
|
}
|
|
|
|
|
2021-07-28 06:57:07 +02:00
|
|
|
let key_width = 15u16; // for showing pending keys
|
|
|
|
let mut status_msg_width = 0;
|
|
|
|
|
2021-05-07 07:19:58 +02:00
|
|
|
// render status msg
|
|
|
|
if let Some((status_msg, severity)) = &cx.editor.status_msg {
|
2021-07-28 06:57:07 +02:00
|
|
|
status_msg_width = status_msg.width();
|
2021-05-07 07:19:58 +02:00
|
|
|
use helix_view::editor::Severity;
|
|
|
|
let style = if *severity == Severity::Error {
|
|
|
|
cx.editor.theme.get("error")
|
|
|
|
} else {
|
2021-05-07 10:38:25 +02:00
|
|
|
cx.editor.theme.get("ui.text")
|
2021-05-07 07:19:58 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
surface.set_string(
|
|
|
|
area.x,
|
|
|
|
area.y + area.height.saturating_sub(1),
|
|
|
|
status_msg,
|
|
|
|
style,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-07-28 06:57:07 +02:00
|
|
|
if area.width.saturating_sub(status_msg_width as u16) > key_width {
|
|
|
|
let mut disp = String::new();
|
|
|
|
if let Some(count) = cx.editor.count {
|
|
|
|
disp.push_str(&count.to_string())
|
|
|
|
}
|
|
|
|
for key in self.keymaps.pending() {
|
2022-06-21 18:46:50 +02:00
|
|
|
disp.push_str(&key.key_sequence_format());
|
2021-07-28 06:57:07 +02:00
|
|
|
}
|
2022-10-03 17:14:57 +02:00
|
|
|
for key in &self.pseudo_pending {
|
|
|
|
disp.push_str(&key.key_sequence_format());
|
2022-03-01 02:29:22 +01:00
|
|
|
}
|
2021-12-12 13:16:48 +01:00
|
|
|
let style = cx.editor.theme.get("ui.text");
|
|
|
|
let macro_width = if cx.editor.macro_recording.is_some() {
|
|
|
|
3
|
|
|
|
} else {
|
|
|
|
0
|
|
|
|
};
|
2021-07-28 06:57:07 +02:00
|
|
|
surface.set_string(
|
2021-12-12 13:16:48 +01:00
|
|
|
area.x + area.width.saturating_sub(key_width + macro_width),
|
2021-07-28 06:57:07 +02:00
|
|
|
area.y + area.height.saturating_sub(1),
|
|
|
|
disp.get(disp.len().saturating_sub(key_width as usize)..)
|
|
|
|
.unwrap_or(&disp),
|
2021-12-12 13:16:48 +01:00
|
|
|
style,
|
2021-07-28 06:57:07 +02:00
|
|
|
);
|
2021-12-12 13:16:48 +01:00
|
|
|
if let Some((reg, _)) = cx.editor.macro_recording {
|
|
|
|
let disp = format!("[{}]", reg);
|
|
|
|
let style = style
|
|
|
|
.fg(helix_view::graphics::Color::Yellow)
|
|
|
|
.add_modifier(Modifier::BOLD);
|
|
|
|
surface.set_string(
|
|
|
|
area.x + area.width.saturating_sub(3),
|
|
|
|
area.y + area.height.saturating_sub(1),
|
|
|
|
&disp,
|
|
|
|
style,
|
|
|
|
);
|
|
|
|
}
|
2021-07-28 06:57:07 +02:00
|
|
|
}
|
|
|
|
|
2021-08-12 09:00:42 +02:00
|
|
|
if let Some(completion) = self.completion.as_mut() {
|
2021-06-19 17:54:37 +02:00
|
|
|
completion.render(area, surface, cx);
|
2021-04-05 11:23:37 +02:00
|
|
|
}
|
2020-12-13 05:29:34 +01:00
|
|
|
}
|
|
|
|
|
2021-07-01 20:57:12 +02:00
|
|
|
fn cursor(&self, _area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
|
2021-12-23 07:26:52 +01:00
|
|
|
match editor.cursor() {
|
|
|
|
// All block cursors are drawn manually
|
|
|
|
(pos, CursorKind::Block) => (pos, CursorKind::Hidden),
|
|
|
|
cursor => cursor,
|
|
|
|
}
|
2020-12-13 04:23:50 +01:00
|
|
|
}
|
|
|
|
}
|
2021-06-03 12:14:02 +02:00
|
|
|
|
2021-06-03 15:26:24 +02:00
|
|
|
fn canonicalize_key(key: &mut KeyEvent) {
|
2021-06-03 12:39:44 +02:00
|
|
|
if let KeyEvent {
|
|
|
|
code: KeyCode::Char(_),
|
|
|
|
modifiers: _,
|
|
|
|
} = key
|
|
|
|
{
|
|
|
|
key.modifiers.remove(KeyModifiers::SHIFT)
|
2021-06-03 12:14:02 +02:00
|
|
|
}
|
2021-06-03 12:39:44 +02:00
|
|
|
}
|