fix: center LSP definition destinations in the viewport
This commit is contained in:
@@ -24,7 +24,7 @@
|
||||
- [x] enable margins in top/bottom bar elements; give the bottom bar's far-right position indicator a default leading margin of 1 column and no trailing margin.
|
||||
- [x] better fuzzy search when searching revisions and bookmarks (or the git alternatives) in before and after picker for diff view.
|
||||
- [x] git blame (enabled by default).
|
||||
- [ ] when jumping to lsp definitions, that line should be center of the buffer (currently placed at the top of the buffer view)
|
||||
- [x] when jumping to lsp definitions, that line should be center of the buffer (currently placed at the top of the buffer view)
|
||||
- [ ] buffer picker (with search).
|
||||
- [ ] pull a pr description and display it in a buffer:
|
||||
- open a selector that allows you to pick a pr from remote
|
||||
|
||||
@@ -338,6 +338,8 @@ See [native LSP architecture](architecture.md#native-lsp) for synchronization an
|
||||
|
||||
`:lsp.definition` or the configured Space d binding requests a definition at the primary caret, once regardless of selection count. Both Location and LocationLink results are supported; multiple results use the first. Local-file navigation reuses loaded buffers without overwriting unsaved text. LSP UTF-16 positions and percent-encoded file URIs are translated to validated grapheme coordinates. Navigation is ignored if its source buffer, revision, caret, or latest request has changed.
|
||||
|
||||
Successful definition jumps center the destination caret's visual row once in its actual text pane, excluding bars and docks. This applies to ordinary buffers, new destination tabs, wrapped text, and split/unified diff panes. Near the beginning of the file, centering clamps to the first line; near EOF, blank space can remain below the centered target. Subsequent cursor motion resumes ordinary scrolling. Failed or stale requests do not request centering; Markdown links, search, and jump-history navigation retain their existing behavior.
|
||||
|
||||
For programmatic bindings, `config.core.lsp` exposes immutable Normal-mode Action objects `definition`, `diagnostics`, and `hover`, plus the **Insert-only** `completion` Action. Normal actions route to their corresponding Ex commands and have no default keys. Completion defaults to Insert Ctrl+n; override or disable it using an Insert binding. It requests directly without leaving Insert or committing pending typing; there is no Normal `:lsp.completion` command. These actions are also available in the command palette according to mode eligibility. The namespace is distinct from `config.lsp[language_id]` server configuration.
|
||||
|
||||
Interactive LSP commands use dotted names (`:lsp.hover`, `:lsp.definition`, `:lsp.diagnostics`). The existing whitespace separator keeps the whole dotted name separate from arguments. Current LSP commands take no arguments.
|
||||
|
||||
@@ -360,6 +360,8 @@ Text panes use normal buffer interaction regardless of writability: motions and
|
||||
|
||||
**LSP definition, hover, and diagnostics work normally in focused Before and After text panes**, including historical snapshots. Each snapshot is served with its immutable revision text and its own document URI, not the current disk contents. Completion and completion edits are available only in writable Working-checkout After; they use the shared buffer's normal undo history. Revision selectors and the changed-file explorer are not document-action targets.
|
||||
|
||||
Accepted LSP definitions center the selection's caret row once in the destination pane, including wrapped visual rows and revealed diff excerpts. Centering uses the focused pane's actual height in either split orientation or unified view. The next motion does not recenter; failed or superseded definitions preserve the current logical viewport.
|
||||
|
||||
A definition whose source range **overlaps an actual added, removed, or replaced line** stays in the same comparison and selects the correct file and side, loading file details on demand. For a location link, the symbol's full target range determines overlap and its selection range determines the caret: an unchanged declaration with a changed body still belongs in the comparison. Source coordinates belong to that side, including old paths for renames; navigation reveals the exact selection row, even when previously omitted, never the nearest excerpt. A wholly unchanged target opens a **new ordinary tab**, even when it is visible context or omitted text in a changed file. Files outside the comparison's changes also open in a new tab. The original comparison, pane layout, source caret, excerpts, and dirty buffers remain intact. Loaded targets share their existing text and undo history rather than reloading disk; references to snapshot URIs retain the historical text and read-only identity in the new tab, even after the comparison closes. Unreadable files, invalid UTF-8, invalid source ranges, and unsupported remote URIs do not create a tab or disturb the source.
|
||||
|
||||
Use the ordinary **file-picker/file-explorer actions and bindings**: Space `o` opens a floating picker, Space `e` opens/focuses the docked explorer, and Ctrl+w closes the focused picker/explorer and reclaims its width. Remapped bindings work too; Ctrl+e is no longer reserved by the diff. The changed-path catalog retains A/M/D/R markers and the normal compact tree, filtering, folder expansion, and configurable `tree`/`selection` navigation. It includes removed and historical paths without scanning the working directory. Enter toggles a folder or opens the selected comparison file and focuses its first visible text pane: After for added files, otherwise Before. While the explorer is hidden, Tab/Shift+Tab cycles between visible text panes only; a single-pane view keeps its focus.
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::plugin::{
|
||||
};
|
||||
use serde::Serialize;
|
||||
use std::borrow::Cow;
|
||||
use std::cell::Cell;
|
||||
use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
|
||||
use std::fmt;
|
||||
use std::fs::{self, OpenOptions};
|
||||
@@ -1337,6 +1338,7 @@ struct ViewState {
|
||||
search_match: Option<SearchMatch>,
|
||||
command_line: String,
|
||||
caret_reveal_revision: u64,
|
||||
lsp_center: Cell<Option<(u64, u64, Cursor)>>,
|
||||
insert_start: Option<Snapshot>,
|
||||
pending_edit: Option<EditRecipe>,
|
||||
visual_motions: Option<Vec<Motion>>,
|
||||
@@ -1354,6 +1356,7 @@ impl ViewState {
|
||||
search_match: None,
|
||||
command_line: String::new(),
|
||||
caret_reveal_revision: 0,
|
||||
lsp_center: Cell::new(None),
|
||||
insert_start: None,
|
||||
pending_edit: None,
|
||||
visual_motions: None,
|
||||
@@ -3527,6 +3530,7 @@ impl Editor {
|
||||
target_range: Option<Utf16Range>,
|
||||
) -> Result<(), String> {
|
||||
self.pending_lsp_navigation = None;
|
||||
self.active.view.lsp_center.set(None);
|
||||
if path.as_os_str().is_empty() || path.to_string_lossy().chars().any(char::is_control) {
|
||||
return Err("Navigation path must be nonempty and contain no controls".into());
|
||||
}
|
||||
@@ -3808,6 +3812,7 @@ impl Editor {
|
||||
diff.normalize_focus();
|
||||
diff.reveal_source_row(focus, row);
|
||||
self.place_navigation(position);
|
||||
self.request_lsp_center();
|
||||
self.record_jump(source);
|
||||
Ok(())
|
||||
}
|
||||
@@ -3829,6 +3834,7 @@ impl Editor {
|
||||
self.switch_buffer(id);
|
||||
}
|
||||
self.place_navigation(position);
|
||||
self.request_lsp_center();
|
||||
self.record_jump(source);
|
||||
}
|
||||
|
||||
@@ -3933,7 +3939,26 @@ impl Editor {
|
||||
self.active.view.caret_reveal_revision
|
||||
}
|
||||
|
||||
fn request_lsp_center(&self) {
|
||||
self.active.view.lsp_center.set(Some((
|
||||
self.text_revision(),
|
||||
self.caret_reveal_revision(),
|
||||
self.cursor(),
|
||||
)));
|
||||
}
|
||||
|
||||
/// Consume only when the destination has a usable text viewport.
|
||||
pub(crate) fn take_lsp_center(&self) -> bool {
|
||||
self.active.view.lsp_center.take()
|
||||
== Some((
|
||||
self.text_revision(),
|
||||
self.caret_reveal_revision(),
|
||||
self.cursor(),
|
||||
))
|
||||
}
|
||||
|
||||
fn reveal_caret(&mut self) {
|
||||
self.active.view.lsp_center.set(None);
|
||||
self.active.view.caret_reveal_revision =
|
||||
self.active.view.caret_reveal_revision.wrapping_add(1);
|
||||
}
|
||||
@@ -5613,6 +5638,7 @@ impl Editor {
|
||||
|
||||
pub fn handle(&mut self, input: Input) -> Option<Invocation> {
|
||||
self.pending_lsp_navigation = None;
|
||||
self.active.view.lsp_center.set(None);
|
||||
// Normalize genuine terminal aliases before list and editing dispatch.
|
||||
let input = match input {
|
||||
Input::Ctrl(ch) => control_key(ch),
|
||||
|
||||
+426
@@ -1561,6 +1561,11 @@ fn draw_document(
|
||||
})
|
||||
})
|
||||
.map(|row| row.min(height.saturating_sub(1)));
|
||||
let center_row = (height > 0 && width > 0 && editor.take_lsp_center()).then_some(height / 2);
|
||||
let toggle_row = center_row.or(toggle_row);
|
||||
if center_row.is_some() {
|
||||
document.manual = None;
|
||||
}
|
||||
if toggle_row.is_some() {
|
||||
document.anchor = Some(identity);
|
||||
}
|
||||
@@ -2665,6 +2670,26 @@ fn draw_diff(
|
||||
(row < usize::from(diff_inner(previous.regions[side].0).height)).then_some(row)
|
||||
}
|
||||
});
|
||||
let center_row = editor.and_then(|editor| {
|
||||
let focus = editor.diff_buffer_focus()?;
|
||||
let pane = if focus == Focus::Before {
|
||||
before_area
|
||||
} else {
|
||||
after_area
|
||||
};
|
||||
let height = usize::from(diff_inner(pane).height);
|
||||
let file = selected.filter(|file| file.loaded)?;
|
||||
(editor.diff_interacting()
|
||||
&& diff.follow_caret
|
||||
&& height > 0
|
||||
&& diff_pane_text_width(diff, file, pane, focus) > 0
|
||||
&& diff
|
||||
.source_display_row(focus, editor.cursor().row)
|
||||
.is_some()
|
||||
&& editor.take_lsp_center())
|
||||
.then_some(height / 2)
|
||||
});
|
||||
let toggle_row = center_row.or(toggle_row);
|
||||
if wrapping && let Some(file) = selected.filter(|file| file.loaded) {
|
||||
let caret = editor
|
||||
.filter(|editor| editor.diff_interacting() && diff.follow_caret)
|
||||
@@ -4502,6 +4527,7 @@ fn semantic_style(token: &SemanticToken) -> Style {
|
||||
#[allow(clippy::single_range_in_vec_init)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::plugin::TextPosition;
|
||||
use std::{
|
||||
fs,
|
||||
path::PathBuf,
|
||||
@@ -8309,6 +8335,406 @@ end
|
||||
assert!(area.contains(terminal.get_cursor_position().unwrap()));
|
||||
}
|
||||
|
||||
fn definition_point(row: usize, column: usize) -> crate::plugin::Utf16Range {
|
||||
crate::plugin::Utf16Range {
|
||||
start: (row, column),
|
||||
end: (row, column),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_definitions_center_in_actual_document_panes_once_and_preserve_dirty_targets() {
|
||||
for (layout, target_kind, row) in [
|
||||
("single", "same", 0),
|
||||
("single", "same", 2),
|
||||
("single", "new", 50),
|
||||
("split", "same", 50),
|
||||
("dock", "dirty", 50),
|
||||
("single", "same", 99),
|
||||
] {
|
||||
let directory = Directory::new();
|
||||
let text = (0..100)
|
||||
.map(|row| format!("definition_{row:03}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
let source = directory.file("source.txt", &text);
|
||||
let target = if target_kind == "same" {
|
||||
source.clone()
|
||||
} else {
|
||||
directory.file("target.txt", &text)
|
||||
};
|
||||
let mut editor = Editor::open(Some(source.clone()), vec![]).unwrap();
|
||||
if target_kind == "dirty" {
|
||||
editor
|
||||
.navigate_to(target.clone(), TextPosition { row: 0, column: 0 })
|
||||
.unwrap();
|
||||
editor.handle(Input::Char('i'));
|
||||
editor.handle(Input::Paste("UNSAVED ".into()));
|
||||
editor.handle(Input::Escape);
|
||||
editor
|
||||
.navigate_to(source, TextPosition { row: 0, column: 0 })
|
||||
.unwrap();
|
||||
}
|
||||
if layout == "split" {
|
||||
editor.execute("pane.split_horizontal");
|
||||
} else if layout == "dock" {
|
||||
editor.set_project_root(directory.0.clone());
|
||||
editor.set_file_explorer_side("bottom").unwrap();
|
||||
editor.handle(Input::Char(' '));
|
||||
editor.handle(Input::Char('e'));
|
||||
editor.handle(Input::Escape);
|
||||
}
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(80, 24)).unwrap();
|
||||
let mut views = BufferViewports::default();
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
editor
|
||||
.navigate_lsp_to(target.clone(), definition_point(row, 0), None)
|
||||
.unwrap();
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
let viewport = views.document.geometry.unwrap().1;
|
||||
if layout != "single" {
|
||||
assert!(viewport.height < 22, "{layout}");
|
||||
}
|
||||
let centered = viewport.y + (row as u16).min(viewport.height / 2);
|
||||
assert_eq!(
|
||||
terminal.get_cursor_position().unwrap().y,
|
||||
centered,
|
||||
"{layout}/{target_kind}/{row}"
|
||||
);
|
||||
assert_eq!(editor.cursor().row, row);
|
||||
assert_eq!(editor.path(), Some(target.as_path()));
|
||||
if target_kind == "dirty" {
|
||||
assert!(editor.lines()[0].starts_with("UNSAVED "));
|
||||
assert_eq!(fs::read_to_string(&target).unwrap(), text);
|
||||
}
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
assert_eq!(terminal.get_cursor_position().unwrap().y, centered);
|
||||
if row == 50 {
|
||||
editor.handle(Input::Down);
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
assert_eq!(terminal.get_cursor_position().unwrap().y, centered + 1);
|
||||
let before = (
|
||||
views.offsets.clone(),
|
||||
screen_text(&terminal),
|
||||
editor.cursor(),
|
||||
);
|
||||
for (path, range) in [
|
||||
(target.clone(), definition_point(1000, 0)),
|
||||
(directory.0.join("missing.txt"), definition_point(0, 0)),
|
||||
] {
|
||||
assert!(editor.navigate_lsp_to(path, range, None).is_err());
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
assert_eq!(views.offsets, before.0);
|
||||
assert_eq!(screen_text(&terminal), before.1);
|
||||
assert_eq!(editor.cursor(), before.2);
|
||||
assert_eq!(terminal.get_cursor_position().unwrap().y, centered + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_definition_centers_wrapped_utf16_grapheme_destination_visually() {
|
||||
let directory = Directory::new();
|
||||
let mut lines = (0..80)
|
||||
.map(|row| format!("row_{row:03}"))
|
||||
.collect::<Vec<_>>();
|
||||
lines[40] = format!("{}😀e\u{301}ZTAIL", "a".repeat(80));
|
||||
let path = directory.file("wrapped.txt", &lines.join("\n"));
|
||||
let mut editor = Editor::open(Some(path.clone()), vec![]).unwrap();
|
||||
editor.execute("word_wrap.toggle");
|
||||
let mut views = BufferViewports::default();
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(24, 12)).unwrap();
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
editor
|
||||
.navigate_lsp_to(path, definition_point(40, 84), None)
|
||||
.unwrap();
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
let viewport = views.document.geometry.unwrap().1;
|
||||
let caret = terminal.get_cursor_position().unwrap();
|
||||
assert_eq!(
|
||||
editor.cursor(),
|
||||
Cursor {
|
||||
row: 40,
|
||||
column: 82
|
||||
}
|
||||
);
|
||||
assert_eq!(caret.y, viewport.y + viewport.height / 2);
|
||||
assert_eq!(
|
||||
terminal.backend().buffer()[(caret.x, caret.y)].symbol(),
|
||||
"Z"
|
||||
);
|
||||
assert_eq!(editor.lines(), lines);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn markdown_search_and_plugin_navigation_keep_ordinary_caret_following() {
|
||||
for navigation in ["markdown", "search", "plugin"] {
|
||||
let directory = Directory::new();
|
||||
let mut lines = (0..80)
|
||||
.map(|row| format!("row_{row:03}"))
|
||||
.collect::<Vec<_>>();
|
||||
lines[0] = "[destination](#destination)".into();
|
||||
lines[50] = "# Destination".into();
|
||||
let path = directory.file("links.md", &lines.join("\n"));
|
||||
let mut editor = Editor::open(Some(path.clone()), vec![]).unwrap();
|
||||
let mut views = BufferViewports::default();
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(80, 16)).unwrap();
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
match navigation {
|
||||
"markdown" => {
|
||||
editor.execute("follow_link");
|
||||
}
|
||||
"search" => {
|
||||
editor.handle(Input::Char('/'));
|
||||
for ch in "Destination".chars() {
|
||||
editor.handle(Input::Char(ch));
|
||||
}
|
||||
editor.handle(Input::Enter);
|
||||
}
|
||||
"plugin" => editor
|
||||
.navigate_to(path, TextPosition { row: 50, column: 0 })
|
||||
.unwrap(),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
let viewport = views.document.geometry.unwrap().1;
|
||||
assert_eq!(editor.cursor().row, 50, "{navigation}");
|
||||
assert_eq!(
|
||||
terminal.get_cursor_position().unwrap().y,
|
||||
viewport.bottom() - 1,
|
||||
"{navigation}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_definition_centers_revealed_diff_symbol_in_each_actual_focused_pane() {
|
||||
for (mode, axis, focus) in [
|
||||
(
|
||||
ViewMode::Split,
|
||||
crate::panes::SplitAxis::Vertical,
|
||||
Focus::Before,
|
||||
),
|
||||
(
|
||||
ViewMode::Split,
|
||||
crate::panes::SplitAxis::Horizontal,
|
||||
Focus::After,
|
||||
),
|
||||
(
|
||||
ViewMode::Unified,
|
||||
crate::panes::SplitAxis::Vertical,
|
||||
Focus::After,
|
||||
),
|
||||
] {
|
||||
let before = (0..90)
|
||||
.map(|row| format!("line_{row:03}"))
|
||||
.collect::<Vec<_>>();
|
||||
let mut after = before.clone();
|
||||
for row in 10..31 {
|
||||
after[row] = format!("changed_{row:03}");
|
||||
}
|
||||
after[70] = "changed_body".into();
|
||||
let mut file = syntax_diff_file("symbol.txt", &before.join("\n"), &after.join("\n"));
|
||||
file.hunks = crate::diff::line_hunks(&file.before, false, &file.after, false);
|
||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||
editor.set_diff_display_options(mode, axis);
|
||||
editor.set_diff_context_options(1, 3);
|
||||
install_readonly_diff(&mut editor, file);
|
||||
editor.handle(Input::Char('\t'));
|
||||
if focus == Focus::After {
|
||||
editor.handle(Input::Char('\t'));
|
||||
}
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(100, 24)).unwrap();
|
||||
let mut views = BufferViewports::default();
|
||||
render_native_diff(&mut terminal, &mut views, &mut editor);
|
||||
let diff = editor.diff().unwrap();
|
||||
let comparison = diff.comparison_id;
|
||||
let side = usize::from(focus == Focus::After);
|
||||
let path = diff.snapshot_roots[side]
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.join("symbol.txt");
|
||||
assert!(diff.source_display_row(focus, 40).is_none());
|
||||
editor
|
||||
.navigate_lsp_to(
|
||||
path.clone(),
|
||||
definition_point(40, 2),
|
||||
Some(crate::plugin::Utf16Range {
|
||||
start: (40, 0),
|
||||
end: (71, 0),
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
let caret = render_native_diff(&mut terminal, &mut views, &mut editor);
|
||||
let geometry = views.panels.diff.as_ref().unwrap();
|
||||
let pane = diff_inner(geometry.regions[if focus == Focus::Before { 1 } else { 2 }].0);
|
||||
assert_eq!(
|
||||
caret.y,
|
||||
pane.y + pane.height / 2,
|
||||
"{mode:?}/{axis:?}/{focus:?}"
|
||||
);
|
||||
assert!(pane.contains(caret));
|
||||
assert_eq!(editor.cursor(), Cursor { row: 40, column: 2 });
|
||||
assert_eq!(editor.diff().unwrap().comparison_id, comparison);
|
||||
assert!(
|
||||
editor
|
||||
.diff()
|
||||
.unwrap()
|
||||
.source_display_row(focus, 40)
|
||||
.is_some()
|
||||
);
|
||||
assert!(editor.mutation_blocked());
|
||||
editor.handle(Input::Down);
|
||||
assert!(render_native_diff(&mut terminal, &mut views, &mut editor).y > caret.y);
|
||||
editor
|
||||
.navigate_lsp_to(path, definition_point(80, 1), None)
|
||||
.unwrap();
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
let viewport = views.document.geometry.unwrap().1;
|
||||
assert!(editor.diff().is_none());
|
||||
assert_eq!(editor.tab_count(), 2);
|
||||
assert_eq!(
|
||||
terminal.get_cursor_position().unwrap().y,
|
||||
viewport.y + viewport.height / 2
|
||||
);
|
||||
assert!(editor.mutation_blocked());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deferred_lsp_definition_centers_only_successful_current_diff_results() {
|
||||
for outcome in ["success", "stale", "failed"] {
|
||||
let directory = Directory::new();
|
||||
let before = (0..90)
|
||||
.map(|row| format!("line_{row:03}"))
|
||||
.collect::<Vec<_>>();
|
||||
let mut after = before.clone();
|
||||
for row in 10..31 {
|
||||
after[row] = format!("changed_{row:03}");
|
||||
}
|
||||
after[70] = "changed_body".into();
|
||||
let mut source = syntax_diff_file("source.txt", &before.join("\n"), &after.join("\n"));
|
||||
source.hunks = crate::diff::line_hunks(&source.before, false, &source.after, false);
|
||||
let mut target = syntax_diff_file("target.txt", &before.join("\n"), &after.join("\n"));
|
||||
target.hunks = crate::diff::line_hunks(&target.before, false, &target.after, false);
|
||||
directory.file("target.txt", &after.join("\n"));
|
||||
let mut unloaded = syntax_diff_file("target.txt", "", "");
|
||||
unloaded.loaded = false;
|
||||
unloaded.before.clear();
|
||||
unloaded.after.clear();
|
||||
unloaded.hunks.clear();
|
||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||
editor.set_diff_context_options(1, 3);
|
||||
editor.execute("diff");
|
||||
let (id, _) = editor.take_diff_request().unwrap();
|
||||
editor.apply_diff_result(id, Ok(crate::diff::Outcome::Catalog(diff_catalog())));
|
||||
editor.handle(Input::Down);
|
||||
editor.handle(Input::Enter);
|
||||
editor.handle(Input::Down);
|
||||
editor.handle(Input::Enter);
|
||||
let (id, _) = editor.take_diff_request().unwrap();
|
||||
editor.apply_diff_result(
|
||||
id,
|
||||
Ok(crate::diff::Outcome::Comparison(crate::diff::Comparison {
|
||||
root: directory.0.clone(),
|
||||
editable_paths: Default::default(),
|
||||
before: "baseline".into(),
|
||||
after: "historic".into(),
|
||||
files: vec![source, unloaded],
|
||||
})),
|
||||
);
|
||||
editor.handle(Input::Char('\t'));
|
||||
let mut views = BufferViewports::default();
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(100, 24)).unwrap();
|
||||
render_native_diff(&mut terminal, &mut views, &mut editor);
|
||||
let comparison_id = editor.diff().unwrap().comparison_id.unwrap();
|
||||
editor
|
||||
.navigate_lsp_to(
|
||||
directory.0.join("target.txt"),
|
||||
definition_point(40, 2),
|
||||
Some(crate::plugin::Utf16Range {
|
||||
start: (40, 0),
|
||||
end: (71, 0),
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
let (id, _) = editor.take_diff_request().unwrap();
|
||||
if outcome == "stale" {
|
||||
editor.handle(Input::Right);
|
||||
editor.handle(Input::Left);
|
||||
}
|
||||
let before_caret = render_native_diff(&mut terminal, &mut views, &mut editor);
|
||||
let before_pane = diff_inner(views.panels.diff.as_ref().unwrap().regions[1].0);
|
||||
let before_viewport = views.panels.diff.as_ref().unwrap().viewport;
|
||||
let before_buffer = editor.buffer_id();
|
||||
let before_source = editor.cursor();
|
||||
editor.apply_diff_result(
|
||||
id,
|
||||
if outcome == "failed" {
|
||||
Err("definition file unavailable".into())
|
||||
} else {
|
||||
Ok(crate::diff::Outcome::File {
|
||||
comparison_id,
|
||||
file_index: 1,
|
||||
file: target,
|
||||
})
|
||||
},
|
||||
);
|
||||
let caret = render_native_diff(&mut terminal, &mut views, &mut editor);
|
||||
if outcome == "success" {
|
||||
let pane = diff_inner(views.panels.diff.as_ref().unwrap().regions[1].0);
|
||||
assert_eq!(editor.cursor(), Cursor { row: 40, column: 2 });
|
||||
assert_eq!(editor.diff().unwrap().file, 1);
|
||||
assert_eq!(caret.y, pane.y + pane.height / 2);
|
||||
editor.handle(Input::Down);
|
||||
assert!(render_native_diff(&mut terminal, &mut views, &mut editor).y > caret.y);
|
||||
} else {
|
||||
assert_eq!(editor.buffer_id(), before_buffer, "{outcome}");
|
||||
assert_eq!(editor.cursor(), before_source, "{outcome}");
|
||||
assert_eq!(editor.diff().unwrap().file, 0, "{outcome}");
|
||||
assert_eq!(
|
||||
views.panels.diff.as_ref().unwrap().viewport,
|
||||
before_viewport,
|
||||
"{outcome}"
|
||||
);
|
||||
let pane = diff_inner(views.panels.diff.as_ref().unwrap().regions[1].0);
|
||||
assert_eq!(
|
||||
(caret.x - pane.x, caret.y - pane.y),
|
||||
(
|
||||
before_caret.x - before_pane.x,
|
||||
before_caret.y - before_pane.y
|
||||
),
|
||||
"{outcome}"
|
||||
);
|
||||
assert_eq!(
|
||||
render_native_diff(&mut terminal, &mut views, &mut editor),
|
||||
caret
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn readonly_diff_editor(file: ChangedFile) -> Editor {
|
||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||
install_readonly_diff(&mut editor, file);
|
||||
|
||||
Reference in New Issue
Block a user