feat: add opt-in trailing whitespace highlighting
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
- [ ] add option to highlight trailing whitespace (off by default)
|
- [x] add option to highlight trailing whitespace (off by default)
|
||||||
- [ ] add option to put characters at certain whitespace characters (off by default)
|
- [ ] 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.
|
||||||
- [ ] github/gitlab/gitea integration (probably requries authentication with github)
|
- [ ] github/gitlab/gitea integration (probably requries authentication with github)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ Vamp.configure do |config|
|
|||||||
config.git_blame = true # false disables current-line attribution
|
config.git_blame = true # false disables current-line attribution
|
||||||
config.git_gutter = "bar" # or "symbols", "line_numbers", "none"
|
config.git_gutter = "bar" # or "symbols", "line_numbers", "none"
|
||||||
config.scroll_distance = 0 # nonnegative edge distance, or nil to center
|
config.scroll_distance = 0 # nonnegative edge distance, or nil to center
|
||||||
|
config.highlight_trailing_whitespace = false # true marks line-end whitespace in red
|
||||||
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"
|
||||||
@@ -52,6 +53,8 @@ Plugin paths resolve relative to the config file, not the shell's working direct
|
|||||||
|
|
||||||
The setting applies to ordinary and historical buffers, split/unified diff panes, file pickers and explorers, command/buffer palettes, completion lists, and revision selectors. Wrapped text measures visual rows, not source lines; picker distances use result rows, excluding borders, search fields, and help. Manual mouse-wheel scrolling remains detached until keyboard navigation resumes. Only `nil` or integers from `0` through the platform's Rust `usize` maximum (`18446744073709551615` on 64-bit systems) are accepted; other values reject the configuration.
|
The setting applies to ordinary and historical buffers, split/unified diff panes, file pickers and explorers, command/buffer palettes, completion lists, and revision selectors. Wrapped text measures visual rows, not source lines; picker distances use result rows, excluding borders, search fields, and help. Manual mouse-wheel scrolling remains detached until keyboard navigation resumes. Only `nil` or integers from `0` through the platform's Rust `usize` maximum (`18446744073709551615` on 64-bit systems) are accepted; other values reject the configuration.
|
||||||
|
|
||||||
|
`config.highlight_trailing_whitespace` defaults to `false`. Set it to `true` to give trailing whitespace a red background in ordinary and historical buffers and both sides of split/unified diffs, including wrapped and horizontally scrolled text. Spaces, expanded tab cells, and Unicode whitespace at the end of a source line are highlighted; whitespace-only lines are included, but empty lines and padding beyond the text are not. Wrapping does not make interior whitespace trailing. Search, selections, and secondary carets retain their background precedence; diagnostic underlines remain visible. This changes display only, not file contents. Only literal Ruby booleans are accepted; other values 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.
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ Vamp.configure do |config|
|
|||||||
config.clipboard_mode = "system" # "separate" keeps ordinary y/d/c/p/P internal
|
config.clipboard_mode = "system" # "separate" keeps ordinary y/d/c/p/P internal
|
||||||
config.git_blame = true # false disables current-line inline blame
|
config.git_blame = true # false disables current-line inline blame
|
||||||
config.scroll_distance = 0 # nonnegative edge distance, or nil to center
|
config.scroll_distance = 0 # nonnegative edge distance, or nil to center
|
||||||
|
config.highlight_trailing_whitespace = false # true marks line-end whitespace in red
|
||||||
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"
|
||||||
|
|||||||
@@ -1763,6 +1763,7 @@ pub struct Editor {
|
|||||||
popup: PopupConfig,
|
popup: PopupConfig,
|
||||||
labels: BTreeMap<String, String>,
|
labels: BTreeMap<String, String>,
|
||||||
scroll_distance: Option<usize>,
|
scroll_distance: Option<usize>,
|
||||||
|
highlight_trailing_whitespace: bool,
|
||||||
battery_charge: Option<u8>,
|
battery_charge: Option<u8>,
|
||||||
battery_label: String,
|
battery_label: String,
|
||||||
git_blame: bool,
|
git_blame: bool,
|
||||||
@@ -1865,6 +1866,7 @@ impl Editor {
|
|||||||
popup: PopupConfig::default(),
|
popup: PopupConfig::default(),
|
||||||
labels: BTreeMap::new(),
|
labels: BTreeMap::new(),
|
||||||
scroll_distance: Some(0),
|
scroll_distance: Some(0),
|
||||||
|
highlight_trailing_whitespace: false,
|
||||||
battery_charge: None,
|
battery_charge: None,
|
||||||
battery_label: String::new(),
|
battery_label: String::new(),
|
||||||
git_blame: true,
|
git_blame: true,
|
||||||
@@ -2611,6 +2613,14 @@ impl Editor {
|
|||||||
self.scroll_distance = distance;
|
self.scroll_distance = distance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn highlight_trailing_whitespace(&self) -> bool {
|
||||||
|
self.highlight_trailing_whitespace
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_highlight_trailing_whitespace(&mut self, enabled: bool) {
|
||||||
|
self.highlight_trailing_whitespace = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
|||||||
+28
-3
@@ -45,6 +45,7 @@ pub struct Config {
|
|||||||
pub popup: PopupConfig,
|
pub popup: PopupConfig,
|
||||||
pub labels: BTreeMap<String, String>,
|
pub labels: BTreeMap<String, String>,
|
||||||
pub scroll_distance: Option<usize>,
|
pub scroll_distance: Option<usize>,
|
||||||
|
pub highlight_trailing_whitespace: bool,
|
||||||
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,
|
||||||
@@ -91,6 +92,7 @@ impl Default for Config {
|
|||||||
popup: PopupConfig::default(),
|
popup: PopupConfig::default(),
|
||||||
labels: BTreeMap::new(),
|
labels: BTreeMap::new(),
|
||||||
scroll_distance: Some(0),
|
scroll_distance: Some(0),
|
||||||
|
highlight_trailing_whitespace: false,
|
||||||
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,
|
||||||
@@ -124,6 +126,7 @@ pub fn load_config(path: &Path) -> Result<Config, String> {
|
|||||||
popup,
|
popup,
|
||||||
labels,
|
labels,
|
||||||
scroll_distance,
|
scroll_distance,
|
||||||
|
highlight_trailing_whitespace,
|
||||||
diff_context_lines,
|
diff_context_lines,
|
||||||
diff_expand_lines,
|
diff_expand_lines,
|
||||||
diff_view_mode,
|
diff_view_mode,
|
||||||
@@ -158,6 +161,7 @@ pub fn load_config(path: &Path) -> Result<Config, String> {
|
|||||||
config.popup = popup;
|
config.popup = popup;
|
||||||
config.labels = labels;
|
config.labels = labels;
|
||||||
config.scroll_distance = scroll_distance;
|
config.scroll_distance = scroll_distance;
|
||||||
|
config.highlight_trailing_whitespace = highlight_trailing_whitespace;
|
||||||
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;
|
||||||
@@ -491,6 +495,7 @@ struct ConfigFrame {
|
|||||||
labels: BTreeMap<String, String>,
|
labels: BTreeMap<String, String>,
|
||||||
#[serde(deserialize_with = "scroll_distance")]
|
#[serde(deserialize_with = "scroll_distance")]
|
||||||
scroll_distance: Option<usize>,
|
scroll_distance: Option<usize>,
|
||||||
|
highlight_trailing_whitespace: bool,
|
||||||
#[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")]
|
||||||
@@ -1234,9 +1239,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,"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,"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,"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,"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,"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,"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}");
|
||||||
}
|
}
|
||||||
@@ -1313,6 +1318,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",
|
"git_gutter": "bar",
|
||||||
"scroll_distance": 0,
|
"scroll_distance": 0,
|
||||||
|
"highlight_trailing_whitespace": false,
|
||||||
"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",
|
||||||
@@ -1331,6 +1337,22 @@ end
|
|||||||
assert!(serde_json::from_value::<Response>(frame).is_err());
|
assert!(serde_json::from_value::<Response>(frame).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn trailing_whitespace_configuration_rejects_non_booleans() {
|
||||||
|
let fixture = Fixture::new();
|
||||||
|
for value in ["nil", "0", "\"true\"", "[]", "{}"] {
|
||||||
|
let path = fixture.file(
|
||||||
|
"config.rb",
|
||||||
|
&format!("Vamp.configure {{ |c| c.highlight_trailing_whitespace = {value} }}"),
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
load_config(&path)
|
||||||
|
.unwrap_err()
|
||||||
|
.contains("highlight_trailing_whitespace must be true or false")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn working_diff_tracking_rejects_non_boolean_configuration() {
|
fn working_diff_tracking_rejects_non_boolean_configuration() {
|
||||||
let fixture = Fixture::new();
|
let fixture = Fixture::new();
|
||||||
@@ -1564,6 +1586,7 @@ end
|
|||||||
"file_picker_view": "tree", "file_explorer_view": "tree",
|
"file_picker_view": "tree", "file_explorer_view": "tree",
|
||||||
"top_bar": {}, "bottom_bar": {}, "labels": {},
|
"top_bar": {}, "bottom_bar": {}, "labels": {},
|
||||||
"scroll_distance": 0,
|
"scroll_distance": 0,
|
||||||
|
"highlight_trailing_whitespace": false,
|
||||||
"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",
|
||||||
@@ -1612,6 +1635,7 @@ end
|
|||||||
"bindings": [], "leader": "space", "file_explorer_side": "right",
|
"bindings": [], "leader": "space", "file_explorer_side": "right",
|
||||||
"file_picker_view": "tree", "file_explorer_view": "tree",
|
"file_picker_view": "tree", "file_explorer_view": "tree",
|
||||||
"clipboard_mode": "system", "git_blame": true, "git_gutter": "bar",
|
"clipboard_mode": "system", "git_blame": true, "git_gutter": "bar",
|
||||||
|
"highlight_trailing_whitespace": false,
|
||||||
"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,
|
||||||
@@ -2040,6 +2064,7 @@ end"#,
|
|||||||
"git_blame": true,
|
"git_blame": true,
|
||||||
"git_gutter": "bar",
|
"git_gutter": "bar",
|
||||||
"scroll_distance": 0,
|
"scroll_distance": 0,
|
||||||
|
"highlight_trailing_whitespace": false,
|
||||||
"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,
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ module Vamp
|
|||||||
@popup = PopupConfig.new
|
@popup = PopupConfig.new
|
||||||
@labels = {}
|
@labels = {}
|
||||||
@scroll_distance = 0
|
@scroll_distance = 0
|
||||||
|
@highlight_trailing_whitespace = false
|
||||||
@diff_context_lines = 10
|
@diff_context_lines = 10
|
||||||
@diff_expand_lines = 15
|
@diff_expand_lines = 15
|
||||||
@diff_view_mode = "split"
|
@diff_view_mode = "split"
|
||||||
@@ -221,6 +222,14 @@ module Vamp
|
|||||||
@scroll_distance = distance.nil? ? nil : diff_line_count!(distance, "scroll_distance", 0)
|
@scroll_distance = distance.nil? ? nil : diff_line_count!(distance, "scroll_distance", 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @param enabled [Boolean]
|
||||||
|
# @return [Boolean]
|
||||||
|
def highlight_trailing_whitespace=(enabled)
|
||||||
|
raise TypeError, "highlight_trailing_whitespace must be true or false" unless [true, false].include?(enabled)
|
||||||
|
|
||||||
|
@highlight_trailing_whitespace = enabled
|
||||||
|
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)
|
||||||
@@ -293,6 +302,7 @@ module Vamp
|
|||||||
"git_blame" => @git_blame,
|
"git_blame" => @git_blame,
|
||||||
"git_gutter" => @git_gutter,
|
"git_gutter" => @git_gutter,
|
||||||
"scroll_distance" => @scroll_distance,
|
"scroll_distance" => @scroll_distance,
|
||||||
|
"highlight_trailing_whitespace" => @highlight_trailing_whitespace,
|
||||||
"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,
|
||||||
|
|||||||
@@ -257,6 +257,7 @@ impl EditorSession {
|
|||||||
editor.set_git_blame(config.git_blame);
|
editor.set_git_blame(config.git_blame);
|
||||||
editor.set_gutter_style(config.git_gutter);
|
editor.set_gutter_style(config.git_gutter);
|
||||||
editor.set_scroll_distance(config.scroll_distance);
|
editor.set_scroll_distance(config.scroll_distance);
|
||||||
|
editor.set_highlight_trailing_whitespace(config.highlight_trailing_whitespace);
|
||||||
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) =
|
||||||
|
|||||||
+125
@@ -1865,6 +1865,9 @@ fn draw_document(
|
|||||||
area,
|
area,
|
||||||
segment.start,
|
segment.start,
|
||||||
);
|
);
|
||||||
|
if editor.highlight_trailing_whitespace() {
|
||||||
|
paint_trailing_whitespace(frame, line, area, segment.start);
|
||||||
|
}
|
||||||
paint_editor_line(
|
paint_editor_line(
|
||||||
frame,
|
frame,
|
||||||
editor,
|
editor,
|
||||||
@@ -2212,6 +2215,21 @@ fn paint_cells(frame: &mut Frame, area: Rect, left: usize, start: usize, end: us
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn paint_trailing_whitespace(frame: &mut Frame, line: &str, area: Rect, left: usize) {
|
||||||
|
let prefix = line.trim_end_matches(char::is_whitespace);
|
||||||
|
if prefix.len() == line.len() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
paint_cells(
|
||||||
|
frame,
|
||||||
|
area,
|
||||||
|
left,
|
||||||
|
display_column(prefix, usize::MAX),
|
||||||
|
display_column(line, usize::MAX),
|
||||||
|
Style::default().bg(Color::Red),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn paint_grapheme_range(
|
fn paint_grapheme_range(
|
||||||
frame: &mut Frame,
|
frame: &mut Frame,
|
||||||
area: Rect,
|
area: Rect,
|
||||||
@@ -2919,6 +2937,7 @@ fn draw_diff(
|
|||||||
),
|
),
|
||||||
wrapped,
|
wrapped,
|
||||||
panels.diff_colors,
|
panels.diff_colors,
|
||||||
|
editor.is_some_and(Editor::highlight_trailing_whitespace),
|
||||||
);
|
);
|
||||||
text_widths[0] = text_area.width;
|
text_widths[0] = text_area.width;
|
||||||
if let Some(editor) =
|
if let Some(editor) =
|
||||||
@@ -2955,6 +2974,7 @@ fn draw_diff(
|
|||||||
),
|
),
|
||||||
wrapped,
|
wrapped,
|
||||||
panels.diff_colors,
|
panels.diff_colors,
|
||||||
|
editor.is_some_and(Editor::highlight_trailing_whitespace),
|
||||||
);
|
);
|
||||||
text_widths[1] = text_area.width;
|
text_widths[1] = text_area.width;
|
||||||
if let Some(editor) =
|
if let Some(editor) =
|
||||||
@@ -3109,6 +3129,7 @@ fn draw_diff_pane(
|
|||||||
viewport: (usize, &mut usize, Option<u16>),
|
viewport: (usize, &mut usize, Option<u16>),
|
||||||
wrapped: Option<&[WrappedDiffRow]>,
|
wrapped: Option<&[WrappedDiffRow]>,
|
||||||
colors: DiffColors,
|
colors: DiffColors,
|
||||||
|
highlight_trailing_whitespace: bool,
|
||||||
) -> Rect {
|
) -> Rect {
|
||||||
if area.width == 0 || area.height == 0 {
|
if area.width == 0 || area.height == 0 {
|
||||||
return area;
|
return area;
|
||||||
@@ -3281,6 +3302,9 @@ fn draw_diff_pane(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if highlight_trailing_whitespace {
|
||||||
|
paint_trailing_whitespace(frame, &text, text_area, left);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Rect::new(
|
Rect::new(
|
||||||
content.x + gutter as u16,
|
content.x + gutter as u16,
|
||||||
@@ -4859,6 +4883,107 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn trailing_whitespace_is_opt_in_and_preserves_wrapping_and_selection() {
|
||||||
|
let directory = Directory::new();
|
||||||
|
let source = "a b\t \u{3000}\n \t\n\nabcdefg ";
|
||||||
|
let path = directory.file("whitespace.txt", source);
|
||||||
|
let config = directory.file(
|
||||||
|
"config.rb",
|
||||||
|
"Vamp.configure { |c| c.highlight_trailing_whitespace = true; c.git_gutter = 'none' }",
|
||||||
|
);
|
||||||
|
let mut session = EditorSession::open(Some(path), vec![], Some(&config)).unwrap();
|
||||||
|
session.execute("word_wrap.toggle");
|
||||||
|
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(10, 10)).unwrap();
|
||||||
|
let mut views = BufferViewports::default();
|
||||||
|
terminal
|
||||||
|
.draw(|frame| views.draw(frame, session.editor_mut()))
|
||||||
|
.unwrap();
|
||||||
|
let buffer = terminal.backend().buffer();
|
||||||
|
for (y, range) in [(0, 5..9), (1, 2..6), (3, 9..10), (4, 2..3)] {
|
||||||
|
for x in 2..10 {
|
||||||
|
// The backend skips the covered cell of a wide grapheme.
|
||||||
|
if (x, y) == (8, 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
buffer[(x, y)].bg == Color::Red,
|
||||||
|
range.contains(&x),
|
||||||
|
"({x}, {y})"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_eq!(buffer[(2, 2)].bg, Color::Reset);
|
||||||
|
session.handle(Input::End);
|
||||||
|
session.handle(Input::Char('v'));
|
||||||
|
session.handle(Input::Left);
|
||||||
|
terminal
|
||||||
|
.draw(|frame| views.draw(frame, session.editor_mut()))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(terminal.backend().buffer()[(6, 0)].bg, Color::Blue);
|
||||||
|
assert_eq!(session.editor().lines().join("\n"), source);
|
||||||
|
assert!(!session.editor().dirty());
|
||||||
|
|
||||||
|
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||||
|
editor
|
||||||
|
.apply(vec![crate::editor::Effect::Replace("x \t".into())])
|
||||||
|
.unwrap();
|
||||||
|
terminal
|
||||||
|
.draw(|frame| views.draw(frame, &mut editor))
|
||||||
|
.unwrap();
|
||||||
|
assert_ne!(terminal.backend().buffer()[(7, 0)].bg, Color::Red);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn trailing_whitespace_covers_both_diff_sides_and_unified_rows() {
|
||||||
|
for mode in [ViewMode::Split, ViewMode::Unified] {
|
||||||
|
for wrapped in [false, true] {
|
||||||
|
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||||
|
editor.set_diff_display_options(mode, crate::panes::SplitAxis::Vertical);
|
||||||
|
install_readonly_diff(
|
||||||
|
&mut editor,
|
||||||
|
syntax_diff_file("whitespace.txt", "old \t", "new \t"),
|
||||||
|
);
|
||||||
|
editor.set_highlight_trailing_whitespace(true);
|
||||||
|
if wrapped {
|
||||||
|
editor.execute("word_wrap.toggle");
|
||||||
|
}
|
||||||
|
let mut terminal =
|
||||||
|
Terminal::new(ratatui::backend::TestBackend::new(80, 15)).unwrap();
|
||||||
|
let mut views = BufferViewports::default();
|
||||||
|
render_native_diff(&mut terminal, &mut views, &mut editor);
|
||||||
|
let buffer = terminal.backend().buffer();
|
||||||
|
for word in ["old", "new"] {
|
||||||
|
let start = buffer
|
||||||
|
.content
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.find_map(|(index, cell)| {
|
||||||
|
(cell.symbol() == &word[..1]
|
||||||
|
&& buffer
|
||||||
|
.content
|
||||||
|
.get(index + 1)
|
||||||
|
.is_some_and(|cell| cell.symbol() == &word[1..2])
|
||||||
|
&& buffer
|
||||||
|
.content
|
||||||
|
.get(index + 2)
|
||||||
|
.is_some_and(|cell| cell.symbol() == &word[2..3]))
|
||||||
|
.then_some(index)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
for offset in 3..8 {
|
||||||
|
assert_eq!(
|
||||||
|
buffer.content[start + offset].bg,
|
||||||
|
Color::Red,
|
||||||
|
"{mode:?} {wrapped} {word}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert_ne!(buffer.content[start + 8].bg, Color::Red);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scroll_distance_follows_document_and_diff_visual_rows_and_columns() {
|
fn scroll_distance_follows_document_and_diff_visual_rows_and_columns() {
|
||||||
let directory = Directory::new();
|
let directory = Directory::new();
|
||||||
|
|||||||
Reference in New Issue
Block a user