feat: preserve diff context during LSP navigation

This commit is contained in:
Valdemar Lorenzen
2026-10-12 14:03:19 +02:00
parent 6eb99a792a
commit 1b8b669ca0
10 changed files with 2559 additions and 126 deletions
+98 -8
View File
@@ -188,6 +188,7 @@ pub struct DiffState {
pub error: Option<String>,
pub comparison: Option<Comparison>,
pub comparison_id: Option<u64>,
pub(crate) snapshot_roots: [Option<PathBuf>; 2],
pub files: Option<FilePicker>,
pub files_visible: bool,
pub file: usize,
@@ -229,6 +230,7 @@ impl DiffState {
comparison: None,
comparison_id: None,
files: None,
snapshot_roots: [None, None],
files_visible: true,
file: 0,
focus: Focus::Files,
@@ -264,6 +266,10 @@ impl DiffState {
self.pending.take()
}
pub(crate) fn request_queued(&self) -> bool {
self.pending.is_some()
}
fn queue(&mut self, operation: Operation) {
self.request_kind = Some(match operation {
Operation::Compare { .. } => RequestKind::Comparison,
@@ -291,7 +297,6 @@ impl DiffState {
..
}) = &result
&& (self.comparison_id != Some(*comparison_id)
|| self.file != *file_index
|| self.request_kind
!= Some(RequestKind::File {
comparison_id: *comparison_id,
@@ -347,6 +352,7 @@ impl DiffState {
self.files = Some(files);
self.comparison = Some(comparison);
self.comparison_id = Some(id);
self.snapshot_roots = [None, None];
self.phase = Phase::View;
self.file = 0;
self.focus = if self.files_visible {
@@ -379,8 +385,10 @@ impl DiffState {
comparison.editable_paths.remove(&file.path);
}
*selected = file;
self.align_file();
self.normalize_focus();
if self.file == file_index {
self.align_file();
self.normalize_focus();
}
self.error = None;
} else {
self.error = Some("Unexpected changed-file response".into());
@@ -742,9 +750,13 @@ impl DiffState {
}
pub fn after_editable(&self) -> bool {
matches!(self.pane_layout(), PaneLayout::Split | PaneLayout::After)
&& self.working_after_available()
}
pub(crate) fn working_after_available(&self) -> bool {
self.phase == Phase::View
&& self.selected_file_loaded()
&& matches!(self.pane_layout(), PaneLayout::Split | PaneLayout::After)
&& self
.after
.as_ref()
@@ -752,6 +764,7 @@ impl DiffState {
&& self.comparison.as_ref().is_some_and(|comparison| {
comparison.files.get(self.file).is_some_and(|file| {
comparison.editable_paths.contains(&file.path)
&& file.status != FileStatus::Removed
&& file
.notice
.as_deref()
@@ -770,6 +783,20 @@ impl DiffState {
}) else {
return false;
};
self.select_file_index(index)
}
pub(crate) fn select_file_index(&mut self, index: usize) -> bool {
let Some(file) = self
.comparison
.as_ref()
.and_then(|comparison| comparison.files.get(index))
else {
return false;
};
if let Some(files) = &mut self.files {
files.reveal_path(&file.path);
}
self.file = index;
self.normalize_focus();
self.live_buffer = None;
@@ -809,6 +836,69 @@ impl DiffState {
}
}
pub(crate) fn load_file(&mut self, file_index: usize) {
if let Some(comparison_id) = self.comparison_id {
self.queue(Operation::LoadFile {
comparison_id,
file_index,
});
}
}
pub(crate) fn source_range_changed(
&self,
file_index: usize,
focus: Focus,
first: usize,
last: usize,
) -> bool {
if file_index == self.file {
return self.rows.iter().any(|aligned| {
aligned.kind != RowKind::Equal
&& match focus {
Focus::Before => aligned
.before
.is_some_and(|row| first <= row && row <= last),
Focus::After => {
aligned.after.is_some_and(|row| first <= row && row <= last)
}
Focus::Files => false,
}
});
}
self.comparison
.as_ref()
.and_then(|comparison| comparison.files.get(file_index))
.is_some_and(|file| {
file.hunks.iter().any(|hunk| {
let (start, len) = match focus {
Focus::Before => (hunk.before_start, hunk.before_len),
Focus::After => (hunk.after_start, hunk.after_len),
Focus::Files => return false,
};
len > 0 && start <= last && first < start.saturating_add(len)
})
})
}
pub(crate) fn reveal_source_row(&mut self, focus: Focus, row: usize) {
if self.source_display_row(focus, row).is_none()
&& let Some(index) = self.rows.iter().position(|aligned| match focus {
Focus::Before => aligned.before == Some(row),
Focus::After => aligned.after == Some(row),
Focus::Files => false,
})
{
self.excerpt_ranges.push((index, index + 1));
self.excerpt_ranges.sort_unstable();
self.rebuild_display_rows();
}
if let Some(display) = self.source_display_row(focus, row) {
self.top = display;
self.follow_caret = true;
}
}
pub(crate) fn source_display_row(&self, focus: Focus, source_row: usize) -> Option<usize> {
self.display_rows.iter().position(|display| {
matches!(*display, DisplayRow::Line(index) if self.source_row(focus, index) == Some(source_row))
@@ -833,14 +923,14 @@ impl DiffState {
})
}
pub(crate) fn snapshot_lines(&self, focus: Focus) -> Vec<String> {
let file = &self.comparison.as_ref().expect("snapshot comparison").files[self.file];
pub(crate) fn snapshot_lines_at(&self, file_index: usize, focus: Focus) -> Vec<String> {
let file = &self.comparison.as_ref().expect("snapshot comparison").files[file_index];
let (source, eof, live) = match focus {
Focus::Before => (&file.before, file.before_eof, false),
Focus::After => (
&file.after,
file.after_eof,
self.live_files.contains(&self.file),
self.live_files.contains(&file_index),
),
Focus::Files => unreachable!("snapshot belongs to a text pane"),
};
@@ -901,7 +991,7 @@ impl DiffState {
}
pub fn refresh_after(&mut self, lines: &[String], revision: u64) {
if !self.after_editable() {
if !self.working_after_available() {
return;
}
if self.live_revision == Some(revision) {
+916 -35
View File
File diff suppressed because it is too large Load Diff
+117 -68
View File
@@ -20,7 +20,7 @@ use crate::{
plugin::{
CompletionEdit, CompletionItem, Diagnostic, DiagnosticSeverity, DocumentEvent,
DocumentEventKind, DocumentSnapshot, PluginUpdate, SemanticToken, SemanticTokenKind,
SemanticTokenModifier, TextPosition, UpdateEffect,
SemanticTokenModifier, TextPosition, UpdateEffect, Utf16Range,
},
snippet,
};
@@ -624,8 +624,8 @@ struct SyncOptions {
enum Request {
Initialize,
Definition(Source),
Hover(Source),
Definition(Source, Arc<DocumentSnapshot>),
Hover(Source, Arc<DocumentSnapshot>),
Completion(Source, Arc<DocumentSnapshot>),
SemanticTokens(Arc<DocumentSnapshot>),
Shutdown,
@@ -1001,8 +1001,8 @@ impl Runtime {
params["context"] = json!({"triggerKind": 1});
}
let request = match kind {
PositionKind::Definition => Request::Definition(source),
PositionKind::Hover => Request::Hover(source),
PositionKind::Definition => Request::Definition(source, Arc::clone(&doc.snapshot)),
PositionKind::Hover => Request::Hover(source, Arc::clone(&doc.snapshot)),
PositionKind::Completion => Request::Completion(source, Arc::clone(&doc.snapshot)),
};
self.request(method, Some(params), request)
@@ -1153,8 +1153,8 @@ impl Runtime {
let context = self.requests.remove(&id).ok_or("unexpected LSP response")?;
if object.contains_key("result") == object.contains_key("error") {
return match context {
Request::Hover(source) => {
self.hover_result(source, Err("unexpected LSP response".into()))
Request::Hover(source, snapshot) => {
self.hover_result(source, snapshot, Err("unexpected LSP response".into()))
}
Request::Completion(source, snapshot) => self.completion_result(
source,
@@ -1173,9 +1173,11 @@ impl Runtime {
.filter(|_| error["code"].as_i64().is_some());
let Some(text) = text else {
return match context {
Request::Hover(source) => {
self.hover_result(source, Err("invalid LSP error response".into()))
}
Request::Hover(source, snapshot) => self.hover_result(
source,
snapshot,
Err("invalid LSP error response".into()),
),
Request::Completion(source, snapshot) => self.completion_result(
source,
snapshot,
@@ -1189,11 +1191,12 @@ impl Runtime {
};
match context {
Request::Initialize => Err(format!("initialization failed: {text}")),
Request::Definition(source) => self.emit(message(
&format!("definition failed: {text}"),
Some(source.request_id),
)),
Request::Hover(source) => self.hover_result(source, Err(text.to_owned())),
Request::Definition(source, snapshot) => {
self.definition_result(source, snapshot, Err(text.to_owned()))
}
Request::Hover(source, snapshot) => {
self.hover_result(source, snapshot, Err(text.to_owned()))
}
Request::Completion(source, snapshot) => {
self.completion_result(source, snapshot, Err(text.to_owned()))
}
@@ -1205,8 +1208,12 @@ impl Runtime {
} else {
match context {
Request::Initialize => self.initialized(&frame["result"]),
Request::Definition(source) => self.definition_result(source, &frame["result"]),
Request::Hover(source) => self.hover_result(source, Ok(&frame["result"])),
Request::Definition(source, snapshot) => {
self.definition_result(source, snapshot, Ok(&frame["result"]))
}
Request::Hover(source, snapshot) => {
self.hover_result(source, snapshot, Ok(&frame["result"]))
}
Request::Completion(source, snapshot) => {
self.completion_result(source, snapshot, Ok(&frame["result"]))
}
@@ -1394,16 +1401,20 @@ impl Runtime {
self.emit_diagnostics(&doc.snapshot, diagnostics)
}
fn definition_result(&self, source: Source, result: &Value) -> Result<(), String> {
fn definition_result(
&self,
source: Source,
snapshot: Arc<DocumentSnapshot>,
result: Result<&Value, String>,
) -> Result<(), String> {
if self
.documents
.get(&source.buffer_id)
.is_none_or(|doc| doc.snapshot.revision != source.revision)
.is_none_or(|doc| !Arc::ptr_eq(&doc.snapshot, &snapshot))
{
return Ok(());
}
let result = self.definition_effects(&source, result);
match result {
match result.and_then(|result| Self::definition_effects(&source, result)) {
Ok(effects) => self.emit(PluginUpdate {
request_id: Some(source.request_id),
effects,
@@ -1415,11 +1426,16 @@ impl Runtime {
}
}
fn hover_result(&self, source: Source, result: Result<&Value, String>) -> Result<(), String> {
fn hover_result(
&self,
source: Source,
snapshot: Arc<DocumentSnapshot>,
result: Result<&Value, String>,
) -> Result<(), String> {
let Some(doc) = self
.documents
.get(&source.buffer_id)
.filter(|doc| doc.snapshot.revision == source.revision)
.filter(|doc| Arc::ptr_eq(&doc.snapshot, &snapshot))
else {
return Ok(());
};
@@ -1440,11 +1456,7 @@ impl Runtime {
}
}
fn definition_effects(
&self,
source: &Source,
result: &Value,
) -> Result<Vec<UpdateEffect>, String> {
fn definition_effects(source: &Source, result: &Value) -> Result<Vec<UpdateEffect>, String> {
let (location, count) = match result {
Value::Null => return Ok(message("no definition found", None).effects),
Value::Array(locations) if locations.is_empty() => {
@@ -1462,53 +1474,25 @@ impl Runtime {
.as_str()
.ok_or("invalid definition URI")?,
)?;
let identity = std::fs::canonicalize(&path).unwrap_or_else(|_| path.clone());
let cached = self.snapshots.values().find(|doc| {
doc.path.as_ref().is_some_and(|candidate| {
let candidate = absolute(Path::new(candidate), &self.root);
std::fs::canonicalize(&candidate).unwrap_or(candidate) == identity
})
});
let disk;
let text = if let Some(doc) = cached {
&doc.text
} else {
if !std::fs::metadata(&path)
.map_err(|error| error.to_string())?
.is_file()
{
return Err("definition target must be a regular file".into());
}
let mut bytes = Vec::new();
std::fs::File::open(&path)
.map_err(|error| error.to_string())?
.take((MAX_BODY + 1) as u64)
.read_to_end(&mut bytes)
.map_err(|error| error.to_string())?;
if bytes.len() > MAX_BODY {
return Err("definition target exceeds 4 MiB".into());
}
disk = String::from_utf8(bytes).map_err(|_| "definition target is not UTF-8")?;
&disk
};
let text = normalized(text);
if link {
range(&text, &location["targetRange"])?;
}
let (start, _) = range(
&text,
// Keep the server's document identity and UTF-16 coordinates together.
// The editor resolves historical/working text, including lazily loaded
// comparison files, before converting the target position.
let target_range = link
.then(|| range_coordinates(&location["targetRange"]))
.transpose()?;
let range = range_coordinates(
&location[if link {
"targetSelectionRange"
} else {
"range"
}],
)?;
let mut effects = vec![UpdateEffect::Navigate {
let mut effects = vec![UpdateEffect::NavigateUtf16 {
buffer_id: source.buffer_id,
revision: source.revision,
path,
row: start.row,
column: start.column,
range,
target_range,
}];
if count > 1 {
effects.push(UpdateEffect::Message {
@@ -2887,10 +2871,17 @@ fn position(text: &str, value: &Value, round_up: bool) -> Result<TextPosition, S
Ok(TextPosition { row, column })
}
fn range(text: &str, value: &Value) -> Result<(TextPosition, TextPosition), String> {
if coordinates(&value["start"])? > coordinates(&value["end"])? {
fn range_coordinates(value: &Value) -> Result<Utf16Range, String> {
let start = coordinates(&value["start"])?;
let end = coordinates(&value["end"])?;
if start > end {
return Err("reversed LSP range".into());
}
Ok(Utf16Range { start, end })
}
fn range(text: &str, value: &Value) -> Result<(TextPosition, TextPosition), String> {
range_coordinates(value)?;
Ok((
position(text, &value["start"], false)?,
position(text, &value["end"], true)?,
@@ -3002,6 +2993,64 @@ fn uri_path(uri: &str) -> Result<PathBuf, String> {
mod tests {
use super::*;
#[test]
fn native_definitions_preserve_snapshot_identity_and_utf16_until_navigation() {
let source = Source {
buffer_id: 7,
revision: 3,
row: 0,
column: 0,
request_id: 11,
};
let path = Path::new("/vamp-immutable-snapshot/Before/renamed/λ.rs");
let span = json!({
"start": {"line": 4, "character": 5},
"end": {"line": 4, "character": 8}
});
for location in [
json!({"uri": file_uri(path).unwrap(), "range": span}),
json!({"targetUri": file_uri(path).unwrap(),
"targetRange": span, "targetSelectionRange": span}),
] {
let effects = Runtime::definition_effects(&source, &location).unwrap();
let expected_target_range = location.get("targetUri").map(|_| Utf16Range {
start: (4, 5),
end: (4, 8),
});
assert!(matches!(
effects.as_slice(),
[UpdateEffect::NavigateUtf16 {
buffer_id: 7, revision: 3, path: target,
range: Utf16Range { start: (4, 5), end: (4, 8) }, target_range,
}] if target == path && *target_range == expected_target_range
));
}
for invalid in [
json!({"start": {"line": 4, "character": 8},
"end": {"line": 4, "character": 5}}),
json!({"start": {"line": -1, "character": 0},
"end": {"line": 0, "character": 0}}),
] {
for location in [
json!({"uri": file_uri(path).unwrap(), "range": invalid}),
json!({"targetUri": file_uri(path).unwrap(),
"targetRange": invalid, "targetSelectionRange": span}),
json!({"targetUri": file_uri(path).unwrap(),
"targetRange": span, "targetSelectionRange": invalid}),
] {
assert!(Runtime::definition_effects(&source, &location).is_err());
}
}
assert!(
serde_json::from_value::<UpdateEffect>(json!({
"type": "navigate_utf16", "buffer_id": 7, "revision": 3,
"path": path, "range": {"start": [4,5], "end": [4,8]},
"target_range": null
}))
.is_err()
);
}
#[test]
fn semantic_decoder_validates_raw_coordinates_before_grapheme_rounding() {
let options = semantic_options(Some(&json!({
+16
View File
@@ -140,6 +140,13 @@ pub struct CompletionItem {
pub snippet: Option<CompletionSnippet>,
}
/// Native LSP range; each coordinate is (zero-based row, UTF-16 column).
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Utf16Range {
pub start: (usize, usize),
pub end: (usize, usize),
}
#[derive(Debug, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
pub enum UpdateEffect {
@@ -162,6 +169,15 @@ pub enum UpdateEffect {
row: usize,
column: usize,
},
/// Native LSP targets retain UTF-16 coordinates until their source text is resolved.
#[serde(skip_deserializing)]
NavigateUtf16 {
buffer_id: usize,
revision: u64,
path: PathBuf,
range: Utf16Range,
target_range: Option<Utf16Range>,
},
Hover {
buffer_id: usize,
revision: u64,
+132 -6
View File
@@ -166,6 +166,7 @@ pub struct EditorSession {
lsp_request_id: u64,
lsp_source_cursor: Option<Cursor>,
lsp_source_mode: Mode,
lsp_source_view: Option<(usize, Option<u64>)>,
pending_hover: bool,
pending_completion: bool,
hover_owner: Option<String>,
@@ -264,6 +265,7 @@ impl EditorSession {
lsp_request_id: 0,
lsp_source_cursor: None,
lsp_source_mode: Mode::Normal,
lsp_source_view: None,
pending_hover: false,
pending_completion: false,
hover_owner: None,
@@ -299,10 +301,12 @@ impl EditorSession {
pub(crate) fn scroll_diff(&mut self, focus: Focus, down: bool) {
self.editor.scroll_diff(focus, down);
self.synchronize_documents();
}
pub(crate) fn scroll_diff_horizontal(&mut self, focus: Focus, right: bool, width: usize) {
self.editor.scroll_diff_horizontal(focus, right, width);
self.synchronize_documents();
}
pub fn handle(&mut self, input: Input) {
@@ -401,11 +405,16 @@ impl EditorSession {
fn dispatch(&mut self, invocation: Invocation) {
if invocation.plugin == "core" {
if !self.editor.document_actions_available()
|| (invocation.command == "lsp.completion" && self.editor.mutation_blocked())
{
return;
}
let result = match invocation.command.as_str() {
"lsp.definition" | "lsp.hover" | "lsp.completion" => {
if let Some(id) = self.lsp_request_id.checked_add(1) {
self.lsp_request_id = id;
let context = self.editor.context();
let context = self.editor.lsp_context();
let source = InvocationSource {
buffer_id: context.buffer_id,
pane_id: self.editor.pane_id(),
@@ -414,6 +423,10 @@ impl EditorSession {
self.requests.entry("core".into()).or_default().latest = Some((id, source));
self.lsp_source_cursor = Some(self.editor.cursor());
self.lsp_source_mode = self.editor.mode();
self.lsp_source_view = Some((
self.editor.tab_id(),
self.editor.diff().and_then(|diff| diff.comparison_id),
));
self.pending_hover = invocation.command == "lsp.hover";
self.pending_completion = invocation.command == "lsp.completion";
match invocation.command.as_str() {
@@ -502,10 +515,17 @@ impl EditorSession {
|| source.revision != self.editor.text_revision()
|| self.lsp_source_cursor != Some(self.editor.cursor())
|| self.editor.mode() != self.lsp_source_mode
|| self.lsp_source_view
!= Some((
self.editor.tab_id(),
self.editor.diff().and_then(|diff| diff.comparison_id),
))
|| !self.editor.document_actions_available()
})
{
requests.latest = None;
self.lsp_source_cursor = None;
self.lsp_source_view = None;
self.pending_hover = false;
self.pending_completion = false;
}
@@ -517,9 +537,15 @@ impl EditorSession {
if !self.editor.should_quit() {
for document in self.editor.interaction_documents() {
let path = document.path.map(Path::to_string_lossy);
let root = self
.editor
.interaction_document_root(document.id)
.to_string_lossy();
let previous = self.documents.get(&document.id);
let renamed = previous
.is_some_and(|previous| previous.snapshot.path.as_deref() != path.as_deref());
let renamed = previous.is_some_and(|previous| {
previous.snapshot.path.as_deref() != path.as_deref()
|| previous.snapshot.root.as_str() != root.as_ref()
});
if renamed {
events.push(DocumentEvent {
event: DocumentEventKind::Close,
@@ -534,7 +560,7 @@ impl EditorSession {
id: document.id,
revision: document.revision,
path: path.map(|path| path.into_owned()),
root: self.editor.project_root().to_string_lossy().into_owned(),
root: root.into_owned(),
text: document.lines.join("\n"),
};
events.push(DocumentEvent {
@@ -636,14 +662,19 @@ impl EditorSession {
request_id: Option<u64>,
source: InvocationSource,
) -> bool {
if self.editor.diff().is_some() && !self.editor.diff_interacting() {
if !self.editor.document_actions_available() {
return false;
}
self.editor.buffer_id() == source.buffer_id
&& self.source_is_current(source)
&& (owner != "core"
|| (self.lsp_source_cursor == Some(self.editor.cursor())
&& self.editor.mode() == self.lsp_source_mode))
&& self.editor.mode() == self.lsp_source_mode
&& self.lsp_source_view
== Some((
self.editor.tab_id(),
self.editor.diff().and_then(|diff| diff.comparison_id),
))))
&& self
.requests
.get(owner)
@@ -755,7 +786,16 @@ impl EditorSession {
revision,
path,
..
}
| UpdateEffect::NavigateUtf16 {
buffer_id,
revision,
path,
..
} => {
if owner != "core" && matches!(effect, UpdateEffect::NavigateUtf16 { .. }) {
return Err("UTF-16 navigation is a native LSP update only".into());
}
let source = InvocationSource {
buffer_id: *buffer_id,
pane_id: self.editor.pane_id(),
@@ -827,6 +867,26 @@ impl EditorSession {
.editor
.replace_validated_semantic_tokens(buffer_id, revision, tokens);
}
UpdateEffect::NavigateUtf16 {
buffer_id,
revision,
path,
range,
target_range,
} => {
let source = InvocationSource {
buffer_id,
pane_id: self.editor.pane_id(),
revision,
};
if self.request_is_current(owner, update.request_id, source) {
if let Err(error) = self.editor.navigate_lsp_to(path, range, target_range) {
self.editor
.set_message(format!("LSP: definition failed: {error}"));
}
changed = true;
}
}
UpdateEffect::Navigate {
buffer_id,
revision,
@@ -1050,6 +1110,68 @@ mod tests {
sync::atomic::{AtomicU64, Ordering},
};
#[test]
fn native_navigation_resolves_utf16_and_validates_full_ranges_atomically() {
use crate::plugin::Utf16Range;
let fixture = Fixture::new();
let source = fixture.file("buffer.txt", "source");
let target = fixture.file("target.txt", "A😀e\u{301}Z\n");
let mut session = EditorSession::open(Some(source.clone()), vec![], None).unwrap();
session.dispatch(Invocation {
plugin: "core".into(),
command: "lsp.definition".into(),
args: String::new(),
});
let buffer_id = session.editor.buffer_id();
let revision = session.editor.text_revision();
let request_id = session.lsp_request_id;
let span = Utf16Range {
start: (0, 5),
end: (0, 6),
};
let invalid = Utf16Range {
end: (9, 0),
..span
};
let update = |range, target_range| PluginUpdate {
request_id: Some(request_id),
effects: vec![UpdateEffect::NavigateUtf16 {
buffer_id,
revision,
path: target.clone(),
range,
target_range,
}],
};
for (range, target_range) in [(invalid, None), (span, Some(invalid))] {
assert!(
session
.apply_update("core", update(range, target_range))
.unwrap()
);
assert_eq!(session.editor.buffer_id(), buffer_id);
assert_eq!(session.editor.path(), Some(source.as_path()));
assert_eq!(session.editor.buffer_count(), 1);
assert_eq!(session.editor.tab_count(), 1);
assert_eq!(session.editor.cursor(), Cursor::default());
assert!(session.editor.message().contains("definition failed"));
}
assert!(session.apply_update("fixture", update(span, None)).is_err());
assert_eq!(session.editor.buffer_id(), buffer_id);
assert!(
session
.apply_update("core", update(span, Some(span)))
.unwrap()
);
assert_eq!(session.editor.path(), Some(target.as_path()));
assert_eq!(session.editor.cursor(), Cursor { row: 0, column: 3 });
assert_eq!(session.editor.tab_count(), 1);
session.handle(Input::Ctrl('o'));
assert_eq!(session.editor.path(), Some(source.as_path()));
assert_eq!(session.editor.cursor(), Cursor::default());
}
static NEXT_FIXTURE: AtomicU64 = AtomicU64::new(0);
#[test]
@@ -1087,6 +1209,10 @@ mod tests {
));
session.lsp_source_cursor = Some(session.editor.cursor());
session.lsp_source_mode = session.editor.mode();
session.lsp_source_view = Some((
session.editor.tab_id(),
session.editor.diff().and_then(|diff| diff.comparison_id),
));
session.editor_mut().with_render_pane(1, |pane| {
assert_eq!(pane.pane_id(), 1);
assert_eq!(pane.lines(), ["source"]);