aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--book/src/configuration.md17
-rw-r--r--book/src/themes.md1
-rw-r--r--helix-core/src/shellwords.rs52
-rw-r--r--helix-term/src/commands.rs11
-rw-r--r--helix-term/src/commands/lsp.rs6
-rw-r--r--helix-term/src/commands/typed.rs10
-rw-r--r--helix-term/src/ui/editor.rs52
-rw-r--r--helix-term/src/ui/info.rs5
-rw-r--r--helix-term/src/ui/markdown.rs5
-rw-r--r--helix-term/src/ui/picker.rs7
-rw-r--r--helix-term/src/ui/popup.rs9
-rw-r--r--helix-term/src/ui/prompt.rs5
-rw-r--r--helix-tui/src/layout.rs16
-rw-r--r--helix-view/src/editor.rs19
-rw-r--r--helix-view/src/graphics.rs67
-rw-r--r--helix-view/src/input.rs27
-rw-r--r--helix-view/src/tree.rs1
-rw-r--r--runtime/themes/onedark.toml3
-rw-r--r--theme.toml2
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"
diff --git a/theme.toml b/theme.toml
index 7a518b2f..8e550f92 100644
--- a/theme.toml
+++ b/theme.toml
@@ -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?