feat: configure spaces on Tab per language
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
- [x] add option to highlight trailing whitespace (off by default)
|
- [x] add option to highlight trailing whitespace (off by default)
|
||||||
- [x] add option to put characters at certain whitespace characters (off by default)
|
- [x] add option to put characters at certain whitespace characters (off by default)
|
||||||
- whitespace kind -> character map should be configurable.
|
- whitespace kind -> character map should be configurable.
|
||||||
- [ ] add option to insert spaces instead tab character when pressing tab:
|
- [x] add option to insert spaces instead tab character when pressing tab:
|
||||||
- number of spaces should be configurable (per language)
|
- number of spaces should be configurable (per language)
|
||||||
- [ ] diff view - design question:
|
- [ ] diff view - design question:
|
||||||
- is it possible to angle the hatchings for empty lines more?
|
- is it possible to angle the hatchings for empty lines more?
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ Vamp.configure do |config|
|
|||||||
config.highlight_trailing_whitespace = false # true marks line-end whitespace in red
|
config.highlight_trailing_whitespace = false # true marks line-end whitespace in red
|
||||||
config.whitespace_characters = {} # opt in: { " " => "·", "\t" => "→", "\n" => "↵" }
|
config.whitespace_characters = {} # opt in: { " " => "·", "\t" => "→", "\n" => "↵" }
|
||||||
config.whitespace_scope = "all" # or "leading" for indentation only
|
config.whitespace_scope = "all" # or "leading" for indentation only
|
||||||
|
config.tab_spaces = nil # opt in with a fixed number of spaces per Tab, e.g. 4
|
||||||
|
config.language_tab_spaces = {} # overrides: { "Ruby" => 2, "Makefile" => nil }
|
||||||
config.file_explorer_side = "right"
|
config.file_explorer_side = "right"
|
||||||
config.file_explorer_view = "tree"
|
config.file_explorer_view = "tree"
|
||||||
config.file_picker_view = "tree"
|
config.file_picker_view = "tree"
|
||||||
@@ -67,6 +69,12 @@ Whitespace inside strings recognized by the bundled syntax grammar stays literal
|
|||||||
|
|
||||||
Markers appear in ordinary and historical buffers and both sides of split/unified diffs. Tabs and wide Unicode whitespace show the marker in their first source cell and leave remaining cells blank, preserving tab stops, wrapping, horizontal scrolling, and cursor/selection geometry. A clipped first cell hides its marker; padding beyond the source does not gain markers. Whitespace with combining characters remains intact. Syntax colors, search, selections, diagnostics, and trailing-whitespace highlighting still apply. This changes display only: editing, copying, and saving retain the original characters.
|
Markers appear in ordinary and historical buffers and both sides of split/unified diffs. Tabs and wide Unicode whitespace show the marker in their first source cell and leave remaining cells blank, preserving tab stops, wrapping, horizontal scrolling, and cursor/selection geometry. A clipped first cell hides its marker; padding beyond the source does not gain markers. Whitespace with combining characters remains intact. Syntax colors, search, selections, diagnostics, and trailing-whitespace highlighting still apply. This changes display only: editing, copying, and saving retain the original characters.
|
||||||
|
|
||||||
|
`config.tab_spaces` defaults to `nil`, preserving literal tabs. Set an integer from `1` through `255` to insert that many spaces per Tab in Insert mode, at every caret. This is a fixed count, not alignment to the next tab stop; it does not change how existing tab characters are displayed.
|
||||||
|
|
||||||
|
`config.language_tab_spaces` defaults to `{}`. It overrides the global setting for the active buffer's detected syntax language, using the case-sensitive names shown in the language bar, for example `{ "Rust" => 4, "Ruby" => 2, "Python" => 4, "Makefile" => nil }`. A `nil` override preserves literal tabs for that language, even when the global setting uses spaces. Unlisted or unknown languages and unnamed buffers without a recognized shebang use `tab_spaces`. Detection reuses syntax highlighting's filename, extension, and shebang rules, independently of whether an LSP is enabled.
|
||||||
|
|
||||||
|
Active snippets retain Tab/Shift+Tab argument navigation; after the final stop, Tab uses the configured insertion setting. Explicit Tab bindings still take precedence, and Ctrl+i remains a Tab alias. Bracketed paste, clipboard paste, completion payloads, and plugin text retain literal tab characters. Insert undo/redo and dot repeat preserve the resolved spaces or tab. Widths other than `nil` or integers in `1..255`, non-hash language overrides, and empty, non-string, or control-containing language keys reject the configuration.
|
||||||
|
|
||||||
`config.git_blame` defaults to `true`; set it to `false` to disable current-line inline Git blame and its background worker. Only literal Ruby booleans are accepted; other values reject the configuration. Toggle it for the running session with `:git_blame.toggle`, its palette action, or a custom binding such as `config.bind "leader b", config.core.git_blame.toggle`. There is no default shortcut.
|
`config.git_blame` defaults to `true`; set it to `false` to disable current-line inline Git blame and its background worker. Only literal Ruby booleans are accepted; other values reject the configuration. Toggle it for the running session with `:git_blame.toggle`, its palette action, or a custom binding such as `config.bind "leader b", config.core.git_blame.toggle`. There is no default shortcut.
|
||||||
|
|
||||||
`config.git_gutter` defaults to `"bar"`: a fat `▌` with one blank column between the line number and marker and another before the text. `"symbols"` uses `+` / `-` / `~` with the same spacing. Added, removed, and modified lines use green, red, and yellow respectively. `"line_numbers"` colors the existing numbers without adding a marker column; the current line retains its cyan/bold caret highlight. `"none"` hides line-gutter repository indicators, not line numbers or picker file statuses. Other modes retain diagnostic number colors independently of repository markers. Unsupported strings, empty strings, and non-string values reject the configuration.
|
`config.git_gutter` defaults to `"bar"`: a fat `▌` with one blank column between the line number and marker and another before the text. `"symbols"` uses `+` / `-` / `~` with the same spacing. Added, removed, and modified lines use green, red, and yellow respectively. `"line_numbers"` colors the existing numbers without adding a marker column; the current line retains its cyan/bold caret highlight. `"none"` hides line-gutter repository indicators, not line numbers or picker file statuses. Other modes retain diagnostic number colors independently of repository markers. Unsupported strings, empty strings, and non-string values reject the configuration.
|
||||||
|
|||||||
+4
-2
@@ -53,6 +53,8 @@ Indicators refresh asynchronously. Typing and undo/redo retain the last complete
|
|||||||
| Insert, Command, Search | Ctrl+u deletes backward to the logical line start |
|
| Insert, Command, Search | Ctrl+u deletes backward to the logical line start |
|
||||||
| Insert, Command, Search (macOS) | Option+Backspace deletes backward by word; Command+Backspace deletes backward to the logical line start |
|
| Insert, Command, Search (macOS) | Option+Backspace deletes backward by word; Command+Backspace deletes backward to the logical line start |
|
||||||
|
|
||||||
|
Outside active snippets, Insert-mode Tab inserts a literal tab by default. Set [`config.tab_spaces` and `config.language_tab_spaces`](configuration.md#preferences) to insert a fixed number of spaces, with per-language overrides. The setting applies to every caret and editable diff text; existing tabs and pasted text are not converted.
|
||||||
|
|
||||||
`J` removes the next line's leading spaces/tabs and preserves existing trailing whitespace. It normally adds one space, two after `.`, `?`, or `!` (Vim's default), and none before `)` or when either side is empty. The cursor lands at the join point; the join is one undo action. On the last line, `J` does nothing.
|
`J` removes the next line's leading spaces/tabs and preserves existing trailing whitespace. It normally adds one space, two after `.`, `?`, or `!` (Vim's default), and none before `)` or when either side is empty. The cursor lands at the join point; the join is one undo action. On the last line, `J` does nothing.
|
||||||
|
|
||||||
`gt`/`gb` move every caret to the first nonblank grapheme on the first/last row, including the empty final row after a trailing newline. Duplicate carets and overlapping Visual selections normalize as usual. Both Visual shapes retain their anchors and extend to the destination. These motions do not edit text or add undo history; count prefixes are consumed rather than interpreted as line numbers. `g` waits for its second key, and Escape cancels only the pending prefix. In Insert and command/search prompts, `gt`/`gb` remain literal text.
|
`gt`/`gb` move every caret to the first nonblank grapheme on the first/last row, including the empty final row after a trailing newline. Duplicate carets and overlapping Visual selections normalize as usual. Both Visual shapes retain their anchors and extend to the destination. These motions do not edit text or add undo history; count prefixes are consumed rather than interpreted as line numbers. `g` waits for its second key, and Escape cancels only the pending prefix. In Insert and command/search prompts, `gt`/`gb` remain literal text.
|
||||||
@@ -65,11 +67,11 @@ The terminal may deliver a native modifier or a conventional control sequence: O
|
|||||||
|
|
||||||
With an enabled server that provides completion, Ctrl+n in Insert requests candidates at the **primary caret only**. The first candidate is inserted immediately and a small list appears near the caret; Ctrl+n or Ctrl+j cycles forward, Ctrl+k backward, with wraparound. There is no acceptance step. The **Original text** row restores the document and carets from before the result was shown, including any additional edits. Completion never broadcasts a candidate to secondary carets: they are preserved and translated through each chosen edit.
|
With an enabled server that provides completion, Ctrl+n in Insert requests candidates at the **primary caret only**. The first candidate is inserted immediately and a small list appears near the caret; Ctrl+n or Ctrl+j cycles forward, Ctrl+k backward, with wraparound. There is no acceptance step. The **Original text** row restores the document and carets from before the result was shown, including any additional edits. Completion never broadcasts a candidate to secondary carets: they are preserved and translated through each chosen edit.
|
||||||
|
|
||||||
Every candidate is applied from that same immutable original document, not on top of the previous candidate. A candidate's primary replacement and additional text edits are one atomic change. Other keys close the list without accepting it: plain-text completion retains ordinary typing, newline, and literal Tab behavior; snippet completion keeps its argument navigation described below. Escape returns to Normal retaining the displayed text. There is no automatic popup. Empty, unsupported, or invalid results leave the document unchanged and report a message.
|
Every candidate is applied from that same immutable original document, not on top of the previous candidate. A candidate's primary replacement and additional text edits are one atomic change. Other keys close the list without accepting it: plain-text completion retains ordinary typing, newline, and configured Tab insertion; snippet completion keeps its argument navigation described below. Escape returns to Normal retaining the displayed text. There is no automatic popup. Empty, unsupported, or invalid results leave the document unchanged and report a message.
|
||||||
|
|
||||||
Cycling creates no separate undo entries. Only the final chosen edits join the existing Insert undo group; choosing Original text adds no completion edit. Undo restores that group, and dot repeats the final resolved completion edits rather than requests or intermediate candidates. The list stays inside the document text workspace, does not cover the gutter or explorer, and never takes the hardware cursor. Wheel input over it is consumed without choosing a candidate.
|
Cycling creates no separate undo entries. Only the final chosen edits join the existing Insert undo group; choosing Original text adds no completion edit. Undo restores that group, and dot repeats the final resolved completion edits rather than requests or intermediate candidates. The list stays inside the document text workspace, does not cover the gutter or explorer, and never takes the hardware cursor. Wheel input over it is consumed without choosing a candidate.
|
||||||
|
|
||||||
Snippet candidates expand the full template immediately, including parentheses and argument defaults. The first numbered argument is highlighted; typing replaces it, then continues within that argument. Repeated occurrences of the same argument update together, without broadcasting to secondary carets. Backspace/Delete stay within the active argument; Enter can insert a newline there. **Tab** moves forward and **Shift+Tab** backward in ascending tabstop order. Replacing an enclosing argument removes its nested arguments, not adjacent independent ones. Reaching the final `$0` position ends navigation, so subsequent Tab inserts a literal tab. Escape, ordinary cursor motion, paste, commands, and focus/buffer changes also end navigation without removing the expanded text.
|
Snippet candidates expand the full template immediately, including parentheses and argument defaults. The first numbered argument is highlighted; typing replaces it, then continues within that argument. Repeated occurrences of the same argument update together, without broadcasting to secondary carets. Backspace/Delete stay within the active argument; Enter can insert a newline there. **Tab** moves forward and **Shift+Tab** backward in ascending tabstop order. Replacing an enclosing argument removes its nested arguments, not adjacent independent ones. Reaching the final `$0` position ends navigation, so subsequent Tab uses the configured spaces-or-tab insertion. Escape, ordinary cursor motion, paste, commands, and focus/buffer changes also end navigation without removing the expanded text.
|
||||||
|
|
||||||
For example, `call(${1:argument}, ${2:count})$0` previews `call(argument, count)`. Type `value`, Tab, `3`, Tab to produce `call(value, 3)` with the caret after the closing parenthesis. Argument edits remain in the same Insert undo group, and dot repeats the resolved text and final caret rather than re-expanding placeholders.
|
For example, `call(${1:argument}, ${2:count})$0` previews `call(argument, count)`. Type `value`, Tab, `3`, Tab to produce `call(value, 3)` with the caret after the closing parenthesis. Argument edits remain in the same Insert undo group, and dot repeats the resolved text and final caret rather than re-expanding placeholders.
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ Vamp.configure do |config|
|
|||||||
config.highlight_trailing_whitespace = false # true marks line-end whitespace in red
|
config.highlight_trailing_whitespace = false # true marks line-end whitespace in red
|
||||||
config.whitespace_characters = {} # opt in: { " " => "·", "\t" => "→", "\n" => "↵" }
|
config.whitespace_characters = {} # opt in: { " " => "·", "\t" => "→", "\n" => "↵" }
|
||||||
config.whitespace_scope = "all" # or "leading" for indentation only; newline markers remain independent
|
config.whitespace_scope = "all" # or "leading" for indentation only; newline markers remain independent
|
||||||
|
config.tab_spaces = nil # opt in: 4 inserts four spaces per Tab
|
||||||
|
config.language_tab_spaces = {} # overrides: { "Ruby" => 2, "Makefile" => nil }
|
||||||
config.file_explorer_side = "right"
|
config.file_explorer_side = "right"
|
||||||
config.file_explorer_view = "tree"
|
config.file_explorer_view = "tree"
|
||||||
config.file_picker_view = "tree"
|
config.file_picker_view = "tree"
|
||||||
|
|||||||
+35
-1
@@ -13,6 +13,7 @@ use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
|
|||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::fs::{self, OpenOptions};
|
use std::fs::{self, OpenOptions};
|
||||||
use std::io::{self, Read, Write};
|
use std::io::{self, Read, Write};
|
||||||
|
use std::num::NonZeroU8;
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use std::os::unix::fs::{MetadataExt, OpenOptionsExt};
|
use std::os::unix::fs::{MetadataExt, OpenOptionsExt};
|
||||||
@@ -1778,6 +1779,8 @@ pub struct Editor {
|
|||||||
highlight_trailing_whitespace: bool,
|
highlight_trailing_whitespace: bool,
|
||||||
whitespace_characters: BTreeMap<char, char>,
|
whitespace_characters: BTreeMap<char, char>,
|
||||||
whitespace_scope: WhitespaceScope,
|
whitespace_scope: WhitespaceScope,
|
||||||
|
tab_spaces: Option<NonZeroU8>,
|
||||||
|
language_tab_spaces: BTreeMap<String, Option<NonZeroU8>>,
|
||||||
battery_charge: Option<u8>,
|
battery_charge: Option<u8>,
|
||||||
battery_label: String,
|
battery_label: String,
|
||||||
git_blame: bool,
|
git_blame: bool,
|
||||||
@@ -1883,6 +1886,8 @@ impl Editor {
|
|||||||
highlight_trailing_whitespace: false,
|
highlight_trailing_whitespace: false,
|
||||||
whitespace_characters: BTreeMap::new(),
|
whitespace_characters: BTreeMap::new(),
|
||||||
whitespace_scope: WhitespaceScope::All,
|
whitespace_scope: WhitespaceScope::All,
|
||||||
|
tab_spaces: None,
|
||||||
|
language_tab_spaces: BTreeMap::new(),
|
||||||
battery_charge: None,
|
battery_charge: None,
|
||||||
battery_label: String::new(),
|
battery_label: String::new(),
|
||||||
git_blame: true,
|
git_blame: true,
|
||||||
@@ -2653,6 +2658,15 @@ impl Editor {
|
|||||||
self.whitespace_scope = scope;
|
self.whitespace_scope = scope;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_tab_spaces(
|
||||||
|
&mut self,
|
||||||
|
spaces: Option<NonZeroU8>,
|
||||||
|
languages: BTreeMap<String, Option<NonZeroU8>>,
|
||||||
|
) {
|
||||||
|
self.tab_spaces = spaces;
|
||||||
|
self.language_tab_spaces = languages;
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn set_diff_context_options(&mut self, context_lines: usize, expand_lines: usize) {
|
pub(crate) fn set_diff_context_options(&mut self, context_lines: usize, expand_lines: usize) {
|
||||||
self.diff_context_options = (context_lines, expand_lines);
|
self.diff_context_options = (context_lines, expand_lines);
|
||||||
if let Some(diff) = &mut self.diff {
|
if let Some(diff) = &mut self.diff {
|
||||||
@@ -8975,6 +8989,25 @@ impl Editor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn type_native(&mut self, text: &str) {
|
fn type_native(&mut self, text: &str) {
|
||||||
|
const SPACES: [u8; u8::MAX as usize] = [b' '; u8::MAX as usize];
|
||||||
|
let text = if text == "\t" {
|
||||||
|
let width = if self.language_tab_spaces.is_empty() {
|
||||||
|
self.tab_spaces
|
||||||
|
} else {
|
||||||
|
crate::syntax::language_name(
|
||||||
|
self.active.path.as_deref(),
|
||||||
|
self.active.lines.first().map(String::as_str),
|
||||||
|
)
|
||||||
|
.and_then(|language| self.language_tab_spaces.get(language))
|
||||||
|
.copied()
|
||||||
|
.unwrap_or(self.tab_spaces)
|
||||||
|
};
|
||||||
|
width.map_or(text, |width| {
|
||||||
|
std::str::from_utf8(&SPACES[..usize::from(width.get())]).expect("spaces are UTF-8")
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
text
|
||||||
|
};
|
||||||
if self.edit_snippet(text, None) {
|
if self.edit_snippet(text, None) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -23994,6 +24027,7 @@ mod tests {
|
|||||||
fn snippet_arguments_navigation_undo_redo_and_dot_use_resolved_text() {
|
fn snippet_arguments_navigation_undo_redo_and_dot_use_resolved_text() {
|
||||||
let directory = Directory::new();
|
let directory = Directory::new();
|
||||||
let mut editor = editor();
|
let mut editor = editor();
|
||||||
|
editor.set_tab_spaces(NonZeroU8::new(2), BTreeMap::new());
|
||||||
type_text(&mut editor, "ica");
|
type_text(&mut editor, "ica");
|
||||||
editor
|
editor
|
||||||
.show_completion(vec![snippet_item(
|
.show_completion(vec![snippet_item(
|
||||||
@@ -24026,7 +24060,7 @@ mod tests {
|
|||||||
assert_eq!(editor.cursor(), Cursor { row: 0, column: 12 });
|
assert_eq!(editor.cursor(), Cursor { row: 0, column: 12 });
|
||||||
type_text(&mut editor, ";\n\ttail");
|
type_text(&mut editor, ";\n\ttail");
|
||||||
editor.handle(Input::Escape);
|
editor.handle(Input::Escape);
|
||||||
let resolved = "call(foo, 5);\n\ttail";
|
let resolved = "call(foo, 5);\n tail";
|
||||||
let caret = editor.cursor();
|
let caret = editor.cursor();
|
||||||
assert_eq!(editor.context().text, resolved);
|
assert_eq!(editor.context().text, resolved);
|
||||||
editor.handle(Input::Char('u'));
|
editor.handle(Input::Char('u'));
|
||||||
|
|||||||
+1
-1
@@ -45,7 +45,7 @@ Visual: motions extend selections, y copy, d/x/Delete cut, c change
|
|||||||
Insert: Ctrl+n requests LSP completion at primary caret, then cycles next; Ctrl+j/k next/previous
|
Insert: Ctrl+n requests LSP completion at primary caret, then cycles next; Ctrl+j/k next/previous
|
||||||
Candidates replace text live; Original text restores it; other keys close and act normally
|
Candidates replace text live; Original text restores it; other keys close and act normally
|
||||||
Snippets expand fully: type replaces argument, Tab/Shift+Tab next/previous, final $0 ends
|
Snippets expand fully: type replaces argument, Tab/Shift+Tab next/previous, final $0 ends
|
||||||
Outside snippets Enter inserts newline, Tab inserts tab; Esc keeps text and returns to Normal
|
Outside snippets Enter inserts newline, Tab inserts a tab or configured spaces; Esc returns to Normal
|
||||||
Insert/Command/Search: Ctrl+u deletes backward to the logical line start
|
Insert/Command/Search: Ctrl+u deletes backward to the logical line start
|
||||||
Search: Enter finds next literal match, Esc cancels (case-sensitive, wraps)
|
Search: Enter finds next literal match, Esc cancels (case-sensitive, wraps)
|
||||||
Commands: :save [PATH], :force_save [PATH], :quit, :force_quit,
|
Commands: :save [PATH], :force_save [PATH], :quit, :force_quit,
|
||||||
|
|||||||
+112
-3
@@ -3,6 +3,7 @@ use crate::editor::{Context, Effect, Feature};
|
|||||||
use crate::plugin::{DocumentEvent, PluginEvent, PluginUpdate, UpdateEffect};
|
use crate::plugin::{DocumentEvent, PluginEvent, PluginUpdate, UpdateEffect};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::io::{self, BufRead, BufReader, Write};
|
use std::io::{self, BufRead, BufReader, Write};
|
||||||
|
use std::num::NonZeroU8;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::{Child, Command, Stdio};
|
use std::process::{Child, Command, Stdio};
|
||||||
use std::sync::mpsc::{self, Receiver, RecvTimeoutError, SyncSender};
|
use std::sync::mpsc::{self, Receiver, RecvTimeoutError, SyncSender};
|
||||||
@@ -48,6 +49,8 @@ pub struct Config {
|
|||||||
pub highlight_trailing_whitespace: bool,
|
pub highlight_trailing_whitespace: bool,
|
||||||
pub whitespace_characters: BTreeMap<char, char>,
|
pub whitespace_characters: BTreeMap<char, char>,
|
||||||
pub whitespace_scope: crate::editor::WhitespaceScope,
|
pub whitespace_scope: crate::editor::WhitespaceScope,
|
||||||
|
pub tab_spaces: Option<NonZeroU8>,
|
||||||
|
pub language_tab_spaces: BTreeMap<String, Option<NonZeroU8>>,
|
||||||
pub diff_context_lines: usize,
|
pub diff_context_lines: usize,
|
||||||
pub diff_expand_lines: usize,
|
pub diff_expand_lines: usize,
|
||||||
pub diff_view_mode: crate::diff::ViewMode,
|
pub diff_view_mode: crate::diff::ViewMode,
|
||||||
@@ -97,6 +100,8 @@ impl Default for Config {
|
|||||||
highlight_trailing_whitespace: false,
|
highlight_trailing_whitespace: false,
|
||||||
whitespace_characters: BTreeMap::new(),
|
whitespace_characters: BTreeMap::new(),
|
||||||
whitespace_scope: crate::editor::WhitespaceScope::All,
|
whitespace_scope: crate::editor::WhitespaceScope::All,
|
||||||
|
tab_spaces: None,
|
||||||
|
language_tab_spaces: BTreeMap::new(),
|
||||||
diff_context_lines: 10,
|
diff_context_lines: 10,
|
||||||
diff_expand_lines: 15,
|
diff_expand_lines: 15,
|
||||||
diff_view_mode: crate::diff::ViewMode::Split,
|
diff_view_mode: crate::diff::ViewMode::Split,
|
||||||
@@ -133,6 +138,8 @@ pub fn load_config(path: &Path) -> Result<Config, String> {
|
|||||||
highlight_trailing_whitespace,
|
highlight_trailing_whitespace,
|
||||||
whitespace_characters,
|
whitespace_characters,
|
||||||
whitespace_scope,
|
whitespace_scope,
|
||||||
|
tab_spaces,
|
||||||
|
language_tab_spaces,
|
||||||
diff_context_lines,
|
diff_context_lines,
|
||||||
diff_expand_lines,
|
diff_expand_lines,
|
||||||
diff_view_mode,
|
diff_view_mode,
|
||||||
@@ -170,6 +177,8 @@ pub fn load_config(path: &Path) -> Result<Config, String> {
|
|||||||
config.highlight_trailing_whitespace = highlight_trailing_whitespace;
|
config.highlight_trailing_whitespace = highlight_trailing_whitespace;
|
||||||
config.whitespace_characters = whitespace_characters;
|
config.whitespace_characters = whitespace_characters;
|
||||||
config.whitespace_scope = whitespace_scope;
|
config.whitespace_scope = whitespace_scope;
|
||||||
|
config.tab_spaces = tab_spaces;
|
||||||
|
config.language_tab_spaces = language_tab_spaces;
|
||||||
config.diff_context_lines = diff_context_lines;
|
config.diff_context_lines = diff_context_lines;
|
||||||
config.diff_expand_lines = diff_expand_lines;
|
config.diff_expand_lines = diff_expand_lines;
|
||||||
config.diff_view_mode = diff_view_mode;
|
config.diff_view_mode = diff_view_mode;
|
||||||
@@ -507,6 +516,9 @@ struct ConfigFrame {
|
|||||||
#[serde(deserialize_with = "whitespace_characters")]
|
#[serde(deserialize_with = "whitespace_characters")]
|
||||||
whitespace_characters: BTreeMap<char, char>,
|
whitespace_characters: BTreeMap<char, char>,
|
||||||
whitespace_scope: crate::editor::WhitespaceScope,
|
whitespace_scope: crate::editor::WhitespaceScope,
|
||||||
|
#[serde(deserialize_with = "Option::deserialize")]
|
||||||
|
tab_spaces: Option<NonZeroU8>,
|
||||||
|
language_tab_spaces: BTreeMap<String, Option<NonZeroU8>>,
|
||||||
#[serde(deserialize_with = "diff_context_lines")]
|
#[serde(deserialize_with = "diff_context_lines")]
|
||||||
diff_context_lines: usize,
|
diff_context_lines: usize,
|
||||||
#[serde(deserialize_with = "diff_expand_lines")]
|
#[serde(deserialize_with = "diff_expand_lines")]
|
||||||
@@ -986,6 +998,98 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tab_spaces_follow_active_language_and_preserve_native_edit_semantics() {
|
||||||
|
let fixture = Fixture::new();
|
||||||
|
let config = fixture.file(
|
||||||
|
"config.rb",
|
||||||
|
r#"
|
||||||
|
Vamp.configure do |c|
|
||||||
|
c.tab_spaces = 4
|
||||||
|
c.language_tab_spaces = { "Ruby" => 2, "Makefile" => nil, "JSON" => 1, "YAML" => 255 }
|
||||||
|
end
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
let widest = " ".repeat(255);
|
||||||
|
for (name, prefix, inserted) in [
|
||||||
|
("default.rs", "", "\t"),
|
||||||
|
("buffer.rs", "", " "),
|
||||||
|
("buffer.rb", "", " "),
|
||||||
|
("Gemfile", "", " "),
|
||||||
|
("script", "#!/usr/bin/env ruby\n", " "),
|
||||||
|
("Makefile", "", "\t"),
|
||||||
|
("plain.txt", "", " "),
|
||||||
|
("buffer.json", "", " "),
|
||||||
|
("buffer.yaml", "", widest.as_str()),
|
||||||
|
] {
|
||||||
|
let source = format!("{prefix}αx\nβy");
|
||||||
|
let path = fixture.file(name, &source);
|
||||||
|
let config = (name != "default.rs").then_some(config.as_path());
|
||||||
|
let mut session = EditorSession::open(Some(path), vec![], config).unwrap();
|
||||||
|
assert!(session.editor().startup_diagnostic().is_none());
|
||||||
|
if !prefix.is_empty() {
|
||||||
|
session.handle(Input::Char('j'));
|
||||||
|
}
|
||||||
|
session.handle(Input::Ctrl('e'));
|
||||||
|
for input in [
|
||||||
|
Input::Char('l'),
|
||||||
|
Input::Char('i'),
|
||||||
|
Input::Char('\t'),
|
||||||
|
Input::Ctrl('i'),
|
||||||
|
Input::Escape,
|
||||||
|
] {
|
||||||
|
session.handle(input);
|
||||||
|
}
|
||||||
|
let expected = format!("{prefix}α{inserted}{inserted}x\nβ{inserted}{inserted}y");
|
||||||
|
assert_eq!(session.editor().context().text, expected, "{name}");
|
||||||
|
session.handle(Input::Char('u'));
|
||||||
|
assert_eq!(session.editor().context().text, source, "{name}");
|
||||||
|
session.handle(Input::Char('U'));
|
||||||
|
assert_eq!(session.editor().context().text, expected, "{name}");
|
||||||
|
session.handle(Input::Char('u'));
|
||||||
|
session.handle(Input::Char('.'));
|
||||||
|
assert_eq!(session.editor().context().text, expected, "{name}");
|
||||||
|
session.handle(Input::Char('i'));
|
||||||
|
session.handle(Input::End);
|
||||||
|
session.handle(Input::Paste("\t".into()));
|
||||||
|
assert_eq!(
|
||||||
|
session.editor().context().text,
|
||||||
|
format!("{prefix}α{inserted}{inserted}x\t\nβ{inserted}{inserted}y\t"),
|
||||||
|
"{name}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tab_spaces_reject_invalid_widths_and_language_maps() {
|
||||||
|
let fixture = Fixture::new();
|
||||||
|
for value in ["0", "-1", "256", "1.5", "true", r#""4""#, "[]", "{}"] {
|
||||||
|
for assignment in [
|
||||||
|
format!("c.tab_spaces = {value}"),
|
||||||
|
format!(r#"c.language_tab_spaces = {{ "Ruby" => {value} }}"#),
|
||||||
|
] {
|
||||||
|
let config = fixture.file(
|
||||||
|
"config.rb",
|
||||||
|
&format!("Vamp.configure {{ |c| {assignment} }}"),
|
||||||
|
);
|
||||||
|
assert!(load_config(&config).is_err(), "{assignment}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for value in [
|
||||||
|
"nil",
|
||||||
|
"[]",
|
||||||
|
r#"{ "" => 2 }"#,
|
||||||
|
r#"{ :Ruby => 2 }"#,
|
||||||
|
r#"{ "\n" => 2 }"#,
|
||||||
|
] {
|
||||||
|
let config = fixture.file(
|
||||||
|
"config.rb",
|
||||||
|
&format!("Vamp.configure {{ |c| c.language_tab_spaces = {value} }}"),
|
||||||
|
);
|
||||||
|
assert!(load_config(&config).is_err(), "{value}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn actual_ruby_dsl_context_queued_effects_and_output_isolation() {
|
fn actual_ruby_dsl_context_queued_effects_and_output_isolation() {
|
||||||
let fixture = Fixture::new();
|
let fixture = Fixture::new();
|
||||||
@@ -1274,9 +1378,9 @@ end
|
|||||||
assert!(load_config(&path).unwrap_err().contains(error), "{value}");
|
assert!(load_config(&path).unwrap_err().contains(error), "{value}");
|
||||||
}
|
}
|
||||||
for frame in [
|
for frame in [
|
||||||
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"bindings":[],"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","git_blame":true,"git_gutter":"bar","scroll_distance":0,"highlight_trailing_whitespace":false,"whitespace_characters":{},"whitespace_scope":"all","top_bar":{},"bottom_bar":{},"popup":{"command":{},"search":{}},"labels":{},"diff_context_lines":10,"diff_expand_lines":15,"diff_view_mode":"split","diff_split_orientation":"vertical","diff_follow_working_copy":true}"#,
|
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"bindings":[],"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","git_blame":true,"git_gutter":"bar","scroll_distance":0,"highlight_trailing_whitespace":false,"whitespace_characters":{},"whitespace_scope":"all","tab_spaces":null,"language_tab_spaces":{},"top_bar":{},"bottom_bar":{},"popup":{"command":{},"search":{}},"labels":{},"diff_context_lines":10,"diff_expand_lines":15,"diff_view_mode":"split","diff_split_orientation":"vertical","diff_follow_working_copy":true}"#,
|
||||||
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"bindings":[],"leader":null,"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","git_blame":true,"git_gutter":"bar","scroll_distance":0,"highlight_trailing_whitespace":false,"whitespace_characters":{},"whitespace_scope":"all","top_bar":{},"bottom_bar":{},"popup":{"command":{},"search":{}},"labels":{},"diff_context_lines":10,"diff_expand_lines":15,"diff_view_mode":"split","diff_split_orientation":"vertical","diff_follow_working_copy":true}"#,
|
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"bindings":[],"leader":null,"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","git_blame":true,"git_gutter":"bar","scroll_distance":0,"highlight_trailing_whitespace":false,"whitespace_characters":{},"whitespace_scope":"all","tab_spaces":null,"language_tab_spaces":{},"top_bar":{},"bottom_bar":{},"popup":{"command":{},"search":{}},"labels":{},"diff_context_lines":10,"diff_expand_lines":15,"diff_view_mode":"split","diff_split_orientation":"vertical","diff_follow_working_copy":true}"#,
|
||||||
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"bindings":[],"leader":7,"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","git_blame":true,"git_gutter":"bar","scroll_distance":0,"highlight_trailing_whitespace":false,"whitespace_characters":{},"whitespace_scope":"all","top_bar":{},"bottom_bar":{},"popup":{"command":{},"search":{}},"labels":{},"diff_context_lines":10,"diff_expand_lines":15,"diff_view_mode":"split","diff_split_orientation":"vertical","diff_follow_working_copy":true}"#,
|
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"bindings":[],"leader":7,"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","git_blame":true,"git_gutter":"bar","scroll_distance":0,"highlight_trailing_whitespace":false,"whitespace_characters":{},"whitespace_scope":"all","tab_spaces":null,"language_tab_spaces":{},"top_bar":{},"bottom_bar":{},"popup":{"command":{},"search":{}},"labels":{},"diff_context_lines":10,"diff_expand_lines":15,"diff_view_mode":"split","diff_split_orientation":"vertical","diff_follow_working_copy":true}"#,
|
||||||
] {
|
] {
|
||||||
assert!(serde_json::from_str::<Response>(frame).is_err(), "{frame}");
|
assert!(serde_json::from_str::<Response>(frame).is_err(), "{frame}");
|
||||||
}
|
}
|
||||||
@@ -1356,6 +1460,7 @@ end
|
|||||||
"highlight_trailing_whitespace": false,
|
"highlight_trailing_whitespace": false,
|
||||||
"whitespace_characters": {},
|
"whitespace_characters": {},
|
||||||
"whitespace_scope": "all",
|
"whitespace_scope": "all",
|
||||||
|
"tab_spaces": null, "language_tab_spaces": {},
|
||||||
"top_bar": {}, "bottom_bar": {}, "popup": {"command": {}, "search": {}},
|
"top_bar": {}, "bottom_bar": {}, "popup": {"command": {}, "search": {}},
|
||||||
"labels": {}, "diff_context_lines": 10, "diff_expand_lines": 15,
|
"labels": {}, "diff_context_lines": 10, "diff_expand_lines": 15,
|
||||||
"diff_view_mode": "split", "diff_split_orientation": "vertical",
|
"diff_view_mode": "split", "diff_split_orientation": "vertical",
|
||||||
@@ -1494,6 +1599,7 @@ end"#,
|
|||||||
"file_explorer_view": "tree", "clipboard_mode": "system", "git_blame": true,
|
"file_explorer_view": "tree", "clipboard_mode": "system", "git_blame": true,
|
||||||
"git_gutter": "bar", "scroll_distance": 0, "highlight_trailing_whitespace": false,
|
"git_gutter": "bar", "scroll_distance": 0, "highlight_trailing_whitespace": false,
|
||||||
"whitespace_characters": {}, "whitespace_scope": "all",
|
"whitespace_characters": {}, "whitespace_scope": "all",
|
||||||
|
"tab_spaces": null, "language_tab_spaces": {},
|
||||||
"top_bar": {}, "bottom_bar": {}, "popup": {"command": {}, "search": {}},
|
"top_bar": {}, "bottom_bar": {}, "popup": {"command": {}, "search": {}},
|
||||||
"labels": {}, "diff_context_lines": 10, "diff_expand_lines": 15,
|
"labels": {}, "diff_context_lines": 10, "diff_expand_lines": 15,
|
||||||
"diff_view_mode": "split", "diff_split_orientation": "vertical",
|
"diff_view_mode": "split", "diff_split_orientation": "vertical",
|
||||||
@@ -1755,6 +1861,7 @@ end
|
|||||||
"highlight_trailing_whitespace": false,
|
"highlight_trailing_whitespace": false,
|
||||||
"whitespace_characters": {},
|
"whitespace_characters": {},
|
||||||
"whitespace_scope": "all",
|
"whitespace_scope": "all",
|
||||||
|
"tab_spaces": null, "language_tab_spaces": {},
|
||||||
"popup": {"command": {}, "search": {}},
|
"popup": {"command": {}, "search": {}},
|
||||||
"clipboard_mode": "system", "diff_context_lines": 0, "diff_expand_lines": 7,
|
"clipboard_mode": "system", "diff_context_lines": 0, "diff_expand_lines": 7,
|
||||||
"diff_view_mode": "split", "diff_split_orientation": "vertical",
|
"diff_view_mode": "split", "diff_split_orientation": "vertical",
|
||||||
@@ -1806,6 +1913,7 @@ end
|
|||||||
"highlight_trailing_whitespace": false,
|
"highlight_trailing_whitespace": false,
|
||||||
"whitespace_characters": {},
|
"whitespace_characters": {},
|
||||||
"whitespace_scope": "all",
|
"whitespace_scope": "all",
|
||||||
|
"tab_spaces": null, "language_tab_spaces": {},
|
||||||
"top_bar": {}, "bottom_bar": {}, "labels": {},
|
"top_bar": {}, "bottom_bar": {}, "labels": {},
|
||||||
"popup": {"command": {}, "search": {}},
|
"popup": {"command": {}, "search": {}},
|
||||||
"diff_context_lines": 10, "diff_expand_lines": 15,
|
"diff_context_lines": 10, "diff_expand_lines": 15,
|
||||||
@@ -2237,6 +2345,7 @@ end"#,
|
|||||||
"highlight_trailing_whitespace": false,
|
"highlight_trailing_whitespace": false,
|
||||||
"whitespace_characters": {},
|
"whitespace_characters": {},
|
||||||
"whitespace_scope": "all",
|
"whitespace_scope": "all",
|
||||||
|
"tab_spaces": null, "language_tab_spaces": {},
|
||||||
"top_bar": {}, "bottom_bar": {}, "labels": {},
|
"top_bar": {}, "bottom_bar": {}, "labels": {},
|
||||||
"popup": {"command": {}, "search": {}},
|
"popup": {"command": {}, "search": {}},
|
||||||
"clipboard_mode": "system", "diff_context_lines": 10, "diff_expand_lines": 15,
|
"clipboard_mode": "system", "diff_context_lines": 10, "diff_expand_lines": 15,
|
||||||
|
|||||||
@@ -145,6 +145,8 @@ module Vamp
|
|||||||
@highlight_trailing_whitespace = false
|
@highlight_trailing_whitespace = false
|
||||||
@whitespace_characters = {}
|
@whitespace_characters = {}
|
||||||
@whitespace_scope = "all"
|
@whitespace_scope = "all"
|
||||||
|
@tab_spaces = nil
|
||||||
|
@language_tab_spaces = {}
|
||||||
@diff_context_lines = 10
|
@diff_context_lines = 10
|
||||||
@diff_expand_lines = 15
|
@diff_expand_lines = 15
|
||||||
@diff_view_mode = "split"
|
@diff_view_mode = "split"
|
||||||
@@ -254,6 +256,25 @@ module Vamp
|
|||||||
@whitespace_scope = scope
|
@whitespace_scope = scope
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @param spaces [Integer, nil] spaces per Tab, or nil for a literal tab
|
||||||
|
# @return [Integer, nil]
|
||||||
|
def tab_spaces=(spaces)
|
||||||
|
@tab_spaces = tab_spaces!(spaces)
|
||||||
|
end
|
||||||
|
|
||||||
|
# @param languages [Hash{String => Integer, nil}] syntax language names to Tab widths
|
||||||
|
# @return [Hash{String => Integer, nil}]
|
||||||
|
def language_tab_spaces=(languages)
|
||||||
|
raise TypeError, "language_tab_spaces must be a Hash" unless languages.is_a?(Hash)
|
||||||
|
|
||||||
|
@language_tab_spaces = languages.to_h do |language, spaces|
|
||||||
|
language = nonempty_string!(language, "Tab language")
|
||||||
|
raise ArgumentError, "Tab language must not contain control characters" if language.match?(/[[:cntrl:]]/)
|
||||||
|
|
||||||
|
[language, tab_spaces!(spaces)]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# @param lines [Integer] nonnegative context lines around each diff change
|
# @param lines [Integer] nonnegative context lines around each diff change
|
||||||
# @return [Integer]
|
# @return [Integer]
|
||||||
def diff_context_lines=(lines)
|
def diff_context_lines=(lines)
|
||||||
@@ -329,6 +350,7 @@ module Vamp
|
|||||||
"highlight_trailing_whitespace" => @highlight_trailing_whitespace,
|
"highlight_trailing_whitespace" => @highlight_trailing_whitespace,
|
||||||
"whitespace_characters" => @whitespace_characters,
|
"whitespace_characters" => @whitespace_characters,
|
||||||
"whitespace_scope" => @whitespace_scope,
|
"whitespace_scope" => @whitespace_scope,
|
||||||
|
"tab_spaces" => @tab_spaces, "language_tab_spaces" => @language_tab_spaces,
|
||||||
"top_bar" => @bar.top, "bottom_bar" => @bar.bottom, "labels" => @labels,
|
"top_bar" => @bar.top, "bottom_bar" => @bar.bottom, "labels" => @labels,
|
||||||
"popup" => { "command" => @popup.command, "search" => @popup.search },
|
"popup" => { "command" => @popup.command, "search" => @popup.search },
|
||||||
"diff_context_lines" => @diff_context_lines, "diff_expand_lines" => @diff_expand_lines,
|
"diff_context_lines" => @diff_context_lines, "diff_expand_lines" => @diff_expand_lines,
|
||||||
@@ -337,6 +359,16 @@ module Vamp
|
|||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
# @param spaces [Integer, nil]
|
||||||
|
# @return [Integer, nil]
|
||||||
|
def tab_spaces!(spaces)
|
||||||
|
unless spaces.nil? || (spaces.is_a?(Integer) && spaces.between?(1, 255))
|
||||||
|
raise ArgumentError, "tab_spaces must be nil or an Integer between 1 and 255"
|
||||||
|
end
|
||||||
|
|
||||||
|
spaces
|
||||||
|
end
|
||||||
|
|
||||||
# @param value [Integer]
|
# @param value [Integer]
|
||||||
# @param label [String]
|
# @param label [String]
|
||||||
# @param minimum [Integer]
|
# @param minimum [Integer]
|
||||||
|
|||||||
@@ -260,6 +260,7 @@ impl EditorSession {
|
|||||||
editor.set_highlight_trailing_whitespace(config.highlight_trailing_whitespace);
|
editor.set_highlight_trailing_whitespace(config.highlight_trailing_whitespace);
|
||||||
editor.set_whitespace_characters(config.whitespace_characters);
|
editor.set_whitespace_characters(config.whitespace_characters);
|
||||||
editor.set_whitespace_scope(config.whitespace_scope);
|
editor.set_whitespace_scope(config.whitespace_scope);
|
||||||
|
editor.set_tab_spaces(config.tab_spaces, config.language_tab_spaces);
|
||||||
editor.set_diff_context_options(config.diff_context_lines, config.diff_expand_lines);
|
editor.set_diff_context_options(config.diff_context_lines, config.diff_expand_lines);
|
||||||
editor.set_diff_display_options(config.diff_view_mode, config.diff_split_orientation);
|
editor.set_diff_display_options(config.diff_view_mode, config.diff_split_orientation);
|
||||||
if let Err(error) =
|
if let Err(error) =
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ pub(crate) fn is_markdown_path(path: &Path) -> bool {
|
|||||||
languages::detect(Some(path), None).is_some_and(|syntax| syntax.name == "Markdown")
|
languages::detect(Some(path), None).is_some_and(|syntax| syntax.name == "Markdown")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn language_name(path: Option<&Path>, first_line: Option<&str>) -> Option<&'static str> {
|
||||||
|
languages::detect(path, first_line).map(|syntax| syntax.name)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub enum SyntaxKind {
|
pub enum SyntaxKind {
|
||||||
Comment,
|
Comment,
|
||||||
|
|||||||
Reference in New Issue
Block a user