diff options
-rw-r--r-- | book/src/configuration.md | 17 | ||||
-rw-r--r-- | book/src/themes.md | 1 | ||||
-rw-r--r-- | helix-core/src/shellwords.rs | 52 | ||||
-rw-r--r-- | helix-term/src/commands.rs | 11 | ||||
-rw-r--r-- | helix-term/src/commands/lsp.rs | 6 | ||||
-rw-r--r-- | helix-term/src/commands/typed.rs | 10 | ||||
-rw-r--r-- | helix-term/src/ui/editor.rs | 52 | ||||
-rw-r--r-- | helix-term/src/ui/info.rs | 5 | ||||
-rw-r--r-- | helix-term/src/ui/markdown.rs | 5 | ||||
-rw-r--r-- | helix-term/src/ui/picker.rs | 7 | ||||
-rw-r--r-- | helix-term/src/ui/popup.rs | 9 | ||||
-rw-r--r-- | helix-term/src/ui/prompt.rs | 5 | ||||
-rw-r--r-- | helix-tui/src/layout.rs | 16 | ||||
-rw-r--r-- | helix-view/src/editor.rs | 19 | ||||
-rw-r--r-- | helix-view/src/graphics.rs | 67 | ||||
-rw-r--r-- | helix-view/src/input.rs | 27 | ||||
-rw-r--r-- | helix-view/src/tree.rs | 1 | ||||
-rw-r--r-- | runtime/themes/onedark.toml | 3 | ||||
-rw-r--r-- | theme.toml | 2 |
19 files changed, 235 insertions, 80 deletions
diff --git a/book/src/configuration.md b/book/src/configuration.md index 4d7e440a..3fa9b307 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -167,3 +167,20 @@ nbsp = "⍽" tab = "→" newline = "⏎" ``` + +### `[editor.indent-guides]` Section + +Options for rendering vertical indent guides. + +| Key | Description | Default | +| --- | --- | --- | +| `render` | Whether to render indent guides. | `false` | +| `character` | Literal character to use for rendering the indent guide | `│` | + +Example: + +```toml +[editor.indent-guides] +render = true +character = "╎" +``` diff --git a/book/src/themes.md b/book/src/themes.md index 4c0eda22..57a8d5d1 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -228,6 +228,7 @@ These scopes are used for theming the editor interface. | `ui.text.info` | The key: command text in `ui.popup.info` boxes | | `ui.virtual.ruler` | Ruler columns (see the [`editor.rulers` config][rulers-config])| | `ui.virtual.whitespace` | Visible white-space characters | +| `ui.virtual.indent-guide` | Vertical indent width guides | | `ui.menu` | Code and command completion menus | | `ui.menu.selected` | Selected autocomplete item | | `ui.selection` | For selections in the editing area | diff --git a/helix-core/src/shellwords.rs b/helix-core/src/shellwords.rs index 13f6f3e9..4323039a 100644 --- a/helix-core/src/shellwords.rs +++ b/helix-core/src/shellwords.rs @@ -24,9 +24,13 @@ pub fn shellwords(input: &str) -> Vec<Cow<'_, str>> { state = match state { Normal => match c { '\\' => { - escaped.push_str(&input[start..i]); - start = i + 1; - NormalEscaped + if cfg!(unix) { + escaped.push_str(&input[start..i]); + start = i + 1; + NormalEscaped + } else { + Normal + } } '"' => { end = i; @@ -45,9 +49,13 @@ pub fn shellwords(input: &str) -> Vec<Cow<'_, str>> { NormalEscaped => Normal, Quoted => match c { '\\' => { - escaped.push_str(&input[start..i]); - start = i + 1; - QuoteEscaped + if cfg!(unix) { + escaped.push_str(&input[start..i]); + start = i + 1; + QuoteEscaped + } else { + Quoted + } } '\'' => { end = i; @@ -58,9 +66,13 @@ pub fn shellwords(input: &str) -> Vec<Cow<'_, str>> { QuoteEscaped => Quoted, Dquoted => match c { '\\' => { - escaped.push_str(&input[start..i]); - start = i + 1; - DquoteEscaped + if cfg!(unix) { + escaped.push_str(&input[start..i]); + start = i + 1; + DquoteEscaped + } else { + Dquoted + } } '"' => { end = i; @@ -99,6 +111,25 @@ mod test { use super::*; #[test] + #[cfg(windows)] + fn test_normal() { + let input = r#":o single_word twó wörds \three\ \"with\ escaping\\"#; + let result = shellwords(input); + let expected = vec![ + Cow::from(":o"), + Cow::from("single_word"), + Cow::from("twó"), + Cow::from("wörds"), + Cow::from("\\three\\"), + Cow::from("\\"), + Cow::from("with\\ escaping\\\\"), + ]; + // TODO test is_owned and is_borrowed, once they get stabilized. + assert_eq!(expected, result); + } + + #[test] + #[cfg(unix)] fn test_normal() { let input = r#":o single_word twó wörds \three\ \"with\ escaping\\"#; let result = shellwords(input); @@ -114,6 +145,7 @@ mod test { } #[test] + #[cfg(unix)] fn test_quoted() { let quoted = r#":o 'single_word' 'twó wörds' '' ' ''\three\' \"with\ escaping\\' 'quote incomplete"#; @@ -129,6 +161,7 @@ mod test { } #[test] + #[cfg(unix)] fn test_dquoted() { let dquoted = r#":o "single_word" "twó wörds" "" " ""\three\' \"with\ escaping\\" "dquote incomplete"#; let result = shellwords(dquoted); @@ -143,6 +176,7 @@ mod test { } #[test] + #[cfg(unix)] fn test_mixed() { let dquoted = r#":o single_word 'twó wörds' "\three\' \"with\ escaping\\""no space before"'and after' $#%^@ "%^&(%^" ')(*&^%''a\\\\\b' '"#; let result = shellwords(dquoted); diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 897712f0..9239b49f 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1747,11 +1747,13 @@ fn global_search(cx: &mut Context) { let smart_case = config.search.smart_case; let file_picker_config = config.file_picker.clone(); - let completions = search_completions(cx, None); + let reg = cx.register.unwrap_or('/'); + + let completions = search_completions(cx, Some(reg)); ui::regex_prompt( cx, "global-search:".into(), - None, + Some(reg), move |_editor: &Editor, input: &str| { completions .iter() @@ -2244,9 +2246,8 @@ pub fn command_palette(cx: &mut Context) { .iter() .map(|bind| { bind.iter() - .map(|key| key.to_string()) - .collect::<Vec<String>>() - .join("+") + .map(|key| key.key_sequence_format()) + .collect::<String>() }) .collect::<Vec<String>>() .join(", ") diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index ff61ee63..b8c5e5d1 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -287,10 +287,8 @@ pub fn code_action(cx: &mut Context) { }); picker.move_down(); // pre-select the first item - let popup = Popup::new("code-action", picker).margin(helix_view::graphics::Margin { - vertical: 1, - horizontal: 1, - }); + let popup = + Popup::new("code-action", picker).margin(helix_view::graphics::Margin::all(1)); compositor.replace_or_push("code-action", popup); }, ) diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 5b48ca48..19c6a5dc 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1804,15 +1804,7 @@ pub fn command_mode(cx: &mut Context) { // Handle typable commands if let Some(cmd) = typed::TYPABLE_COMMAND_MAP.get(parts[0]) { - let args = if cfg!(unix) { - shellwords::shellwords(input) - } else { - // Windows doesn't support POSIX, so fallback for now - parts - .into_iter() - .map(|part| part.into()) - .collect::<Vec<_>>() - }; + let args = shellwords::shellwords(input); if let Err(e) = (cmd.fun)(cx, &args[1..], event) { cx.editor.set_error(format!("{}", e)); diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index a8027d1b..dc6362c6 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -13,7 +13,6 @@ use helix_core::{ }, movement::Direction, syntax::{self, HighlightEvent}, - unicode::segmentation::UnicodeSegmentation, unicode::width::UnicodeWidthStr, LineEnding, Position, Range, Selection, Transaction, }; @@ -131,7 +130,7 @@ impl EditorView { surface, theme, highlights, - &editor.config().whitespace, + &editor.config(), ); Self::render_gutter(editor, doc, view, view.area, surface, theme, is_focused); Self::render_rulers(editor, doc, view, inner, surface, theme); @@ -373,8 +372,9 @@ impl EditorView { surface: &mut Surface, theme: &Theme, highlights: H, - whitespace: &helix_view::editor::WhitespaceConfig, + config: &helix_view::editor::Config, ) { + let whitespace = &config.whitespace; use helix_view::editor::WhitespaceRenderValue; // It's slightly more efficient to produce a full RopeSlice from the Rope, then slice that a bunch @@ -397,10 +397,30 @@ impl EditorView { } else { " ".to_string() }; + let indent_guide_char = config.indent_guides.character.to_string(); let text_style = theme.get("ui.text"); let whitespace_style = theme.get("ui.virtual.whitespace"); + let mut is_in_indent_area = true; + let mut last_line_indent_level = 0; + let indent_style = theme.get("ui.virtual.indent-guide"); + + let draw_indent_guides = |indent_level, line, surface: &mut Surface| { + if !config.indent_guides.render { + return; + } + + for i in 0..(indent_level / tab_width as u16) { + surface.set_string( + viewport.x + (i * tab_width as u16) - offset.col as u16, + viewport.y + line, + &indent_guide_char, + indent_style, + ); + } + }; + 'outer: for event in highlights { match event { HighlightEvent::HighlightStart(span) => { @@ -453,8 +473,18 @@ impl EditorView { ); } + // This is an empty line; draw indent guides at previous line's + // indent level to avoid breaking the guides on blank lines. + if visual_x == 0 { + draw_indent_guides(last_line_indent_level, line, surface); + } else if is_in_indent_area { + // A line with whitespace only + draw_indent_guides(visual_x, line, surface); + } + visual_x = 0; line += 1; + is_in_indent_area = true; // TODO: with proper iter this shouldn't be necessary if line >= viewport.height { @@ -464,7 +494,7 @@ impl EditorView { let grapheme = Cow::from(grapheme); let is_whitespace; - let (grapheme, width) = if grapheme == "\t" { + let (display_grapheme, width) = if grapheme == "\t" { is_whitespace = true; // make sure we display tab as appropriate amount of spaces let visual_tab_width = tab_width - (visual_x as usize % tab_width); @@ -491,7 +521,7 @@ impl EditorView { surface.set_string( viewport.x + visual_x - offset.col as u16, viewport.y + line, - grapheme, + display_grapheme, if is_whitespace { style.patch(whitespace_style) } else { @@ -499,6 +529,11 @@ impl EditorView { }, ); } + 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; + } visual_x = visual_x.saturating_add(width as u16); } @@ -1355,12 +1390,7 @@ impl Component for EditorView { disp.push_str(&count.to_string()) } for key in self.keymaps.pending() { - let s = key.to_string(); - if s.graphemes(true).count() > 1 { - disp.push_str(&format!("<{}>", s)); - } else { - disp.push_str(&s); - } + disp.push_str(&key.key_sequence_format()); } if let Some(pseudo_pending) = &cx.editor.pseudo_pending { disp.push_str(pseudo_pending.as_str()) diff --git a/helix-term/src/ui/info.rs b/helix-term/src/ui/info.rs index 272244c1..cc6b7483 100644 --- a/helix-term/src/ui/info.rs +++ b/helix-term/src/ui/info.rs @@ -27,10 +27,7 @@ impl Component for Info { .borders(Borders::ALL) .border_style(popup_style); - let margin = Margin { - vertical: 0, - horizontal: 1, - }; + let margin = Margin::horizontal(1); let inner = block.inner(area).inner(&margin); block.render(area, surface); diff --git a/helix-term/src/ui/markdown.rs b/helix-term/src/ui/markdown.rs index 037e2f13..e3ce2cd5 100644 --- a/helix-term/src/ui/markdown.rs +++ b/helix-term/src/ui/markdown.rs @@ -323,10 +323,7 @@ impl Component for Markdown { .wrap(Wrap { trim: false }) .scroll((cx.scroll.unwrap_or_default() as u16, 0)); - let margin = Margin { - vertical: 1, - horizontal: 1, - }; + let margin = Margin::all(1); par.render(area.inner(&margin), surface); } diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 9ffe45c1..ebff9827 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -200,10 +200,7 @@ impl<T: 'static> Component for FilePicker<T> { // calculate the inner area inside the box let inner = block.inner(preview_area); // 1 column gap on either side - let margin = Margin { - vertical: 0, - horizontal: 1, - }; + let margin = Margin::horizontal(1); let inner = inner.inner(&margin); block.render(preview_area, surface); @@ -240,7 +237,7 @@ impl<T: 'static> Component for FilePicker<T> { surface, &cx.editor.theme, highlights, - &cx.editor.config().whitespace, + &cx.editor.config(), ); // highlight the line diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs index 185ec15d..f5b79526 100644 --- a/helix-term/src/ui/popup.rs +++ b/helix-term/src/ui/popup.rs @@ -27,10 +27,7 @@ impl<T: Component> Popup<T> { Self { contents, position: None, - margin: Margin { - vertical: 0, - horizontal: 0, - }, + margin: Margin::none(), size: (0, 0), child_size: (0, 0), scroll: 0, @@ -163,8 +160,8 @@ impl<T: Component> Component for Popup<T> { self.child_size = (width, height); self.size = ( - (width + self.margin.horizontal * 2).min(max_width), - (height + self.margin.vertical * 2).min(max_height), + (width + self.margin.width()).min(max_width), + (height + self.margin.height()).min(max_height), ); // re-clamp scroll offset diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index 64154bae..7744a161 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -430,10 +430,7 @@ impl Prompt { .borders(Borders::ALL) .border_style(background); - let inner = block.inner(area).inner(&Margin { - vertical: 0, - horizontal: 1, - }); + let inner = block.inner(area).inner(&Margin::horizontal(1)); block.render(area, surface); text.render(inner, surface, cx); diff --git a/helix-tui/src/layout.rs b/helix-tui/src/layout.rs index e6a84aa0..7c72a778 100644 --- a/helix-tui/src/layout.rs +++ b/helix-tui/src/layout.rs @@ -68,10 +68,7 @@ impl Default for Layout { fn default() -> Layout { Layout { direction: Direction::Vertical, - margin: Margin { - horizontal: 0, - vertical: 0, - }, + margin: Margin::none(), constraints: Vec::new(), } } @@ -87,20 +84,19 @@ impl Layout { } pub fn margin(mut self, margin: u16) -> Layout { - self.margin = Margin { - horizontal: margin, - vertical: margin, - }; + self.margin = Margin::all(margin); self } pub fn horizontal_margin(mut self, horizontal: u16) -> Layout { - self.margin.horizontal = horizontal; + self.margin.left = horizontal; + self.margin.right = horizontal; self } pub fn vertical_margin(mut self, vertical: u16) -> Layout { - self.margin.vertical = vertical; + self.margin.top = vertical; + self.margin.bottom = vertical; self } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 8ef4413e..c5a458d7 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -156,6 +156,8 @@ pub struct Config { pub rulers: Vec<u16>, #[serde(default)] pub whitespace: WhitespaceConfig, + /// Vertical indent width guides. + pub indent_guides: IndentGuidesConfig, } #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] @@ -364,6 +366,22 @@ impl Default for WhitespaceCharacters { } } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(default)] +pub struct IndentGuidesConfig { + pub render: bool, + pub character: char, +} + +impl Default for IndentGuidesConfig { + fn default() -> Self { + Self { + render: false, + character: '│', + } + } +} + impl Default for Config { fn default() -> Self { Self { @@ -391,6 +409,7 @@ impl Default for Config { lsp: LspConfig::default(), rulers: Vec::new(), whitespace: WhitespaceConfig::default(), + indent_guides: IndentGuidesConfig::default(), } } } diff --git a/helix-view/src/graphics.rs b/helix-view/src/graphics.rs index 6d0a9292..7033b7a4 100644 --- a/helix-view/src/graphics.rs +++ b/helix-view/src/graphics.rs @@ -27,8 +27,61 @@ impl Default for CursorKind { #[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Margin {
- pub vertical: u16,
- pub horizontal: u16,
+ pub left: u16,
+ pub right: u16,
+ pub top: u16,
+ pub bottom: u16,
+}
+
+impl Margin {
+ pub fn none() -> Self {
+ Self {
+ left: 0,
+ right: 0,
+ top: 0,
+ bottom: 0,
+ }
+ }
+
+ /// Set uniform margin for all sides.
+ pub fn all(value: u16) -> Self {
+ Self {
+ left: value,
+ right: value,
+ top: value,
+ bottom: value,
+ }
+ }
+
+ /// Set the margin of left and right sides to specified value.
+ pub fn horizontal(value: u16) -> Self {
+ Self {
+ left: value,
+ right: value,
+ top: 0,
+ bottom: 0,
+ }
+ }
+
+ /// Set the margin of top and bottom sides to specified value.
+ pub fn vertical(value: u16) -> Self {
+ Self {
+ left: 0,
+ right: 0,
+ top: value,
+ bottom: value,
+ }
+ }
+
+ /// Get the total width of the margin (left + right)
+ pub fn width(&self) -> u16 {
+ self.left + self.right
+ }
+
+ /// Get the total height of the margin (top + bottom)
+ pub fn height(&self) -> u16 {
+ self.top + self.bottom
+ }
}
/// A simple rectangle used in the computation of the layout and to give widgets an hint about the
@@ -141,14 +194,14 @@ impl Rect { }
pub fn inner(self, margin: &Margin) -> Rect {
- if self.width < 2 * margin.horizontal || self.height < 2 * margin.vertical {
+ if self.width < margin.width() || self.height < margin.height() {
Rect::default()
} else {
Rect {
- x: self.x + margin.horizontal,
- y: self.y + margin.vertical,
- width: self.width - 2 * margin.horizontal,
- height: self.height - 2 * margin.vertical,
+ x: self.x + margin.left,
+ y: self.y + margin.top,
+ width: self.width - margin.width(),
+ height: self.height - margin.height(),
}
}
}
diff --git a/helix-view/src/input.rs b/helix-view/src/input.rs index 5b867930..093006c4 100644 --- a/helix-view/src/input.rs +++ b/helix-view/src/input.rs @@ -1,6 +1,6 @@ //! Input event handling, currently backed by crossterm. use anyhow::{anyhow, Error}; -use helix_core::unicode::width::UnicodeWidthStr; +use helix_core::unicode::{segmentation::UnicodeSegmentation, width::UnicodeWidthStr}; use serde::de::{self, Deserialize, Deserializer}; use std::fmt; @@ -22,6 +22,31 @@ impl KeyEvent { _ => None, } } + + /// Format the key in such a way that a concatenated sequence + /// of keys can be read easily. + /// + /// ``` + /// # use std::str::FromStr; + /// # use helix_view::input::KeyEvent; + /// + /// let k = KeyEvent::from_str("w").unwrap().key_sequence_format(); + /// assert_eq!(k, "w"); + /// + /// let k = KeyEvent::from_str("C-w").unwrap().key_sequence_format(); + /// assert_eq!(k, "<C-w>"); + /// + /// let k = KeyEvent::from_str(" ").unwrap().key_sequence_format(); + /// assert_eq!(k, "<space>"); + /// ``` + pub fn key_sequence_format(&self) -> String { + let s = self.to_string(); + if s.graphemes(true).count() > 1 { + format!("<{}>", s) + } else { + s + } + } } pub(crate) mod keys { diff --git a/helix-view/src/tree.rs b/helix-view/src/tree.rs index e6dba916..3ba85b56 100644 --- a/helix-view/src/tree.rs +++ b/helix-view/src/tree.rs @@ -233,7 +233,6 @@ impl Tree { { if let Some(pos) = container.children.iter().position(|&child| child == index) { container.children.remove(pos); - // TODO: if container now only has one child, remove it and place child in parent if container.children.is_empty() && parent_id != self.root { // if container now empty, remove it diff --git a/runtime/themes/onedark.toml b/runtime/themes/onedark.toml index 280f6914..d0cbb949 100644 --- a/runtime/themes/onedark.toml +++ b/runtime/themes/onedark.toml @@ -47,6 +47,8 @@ diagnostic = { modifiers = ["underlined"] } "error" = { fg = "red", modifiers = ["bold"] } "ui.background" = { bg = "black" } +"ui.virtual" = { fg = "faint-gray" } +"ui.virtual.indent-guide" = { fg = "faint-gray" } "ui.virtual.whitespace" = { fg = "light-gray" } "ui.virtual.ruler" = { bg = "gray" } @@ -85,5 +87,6 @@ white = "#ABB2BF" black = "#282C34" light-black = "#2C323C" gray = "#3E4452" +faint-gray = "#3B4048" light-gray = "#5C6370" linenr = "#4B5263" @@ -56,6 +56,8 @@ label = "honey" "ui.text.focus" = { fg = "white" } "ui.virtual" = { fg = "comet" } +"ui.virtual.indent-guide" = { fg = "comet" } + "ui.selection" = { bg = "#540099" } "ui.selection.primary" = { bg = "#540099" } # TODO: namespace ui.cursor as ui.selection.cursor? |