1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
//! Namespace-preserving LSP jumps and source-context inheritance.
use super::{lsp_language, state};
use crate::editor::{trace, Editor};
use std::path::{Path, PathBuf};
use strop_workspace::{Filesystem, ResourceLocation};
impl Editor {
pub(crate) fn jump_to_location(
&mut self,
location: strop_lsp::ServerLocation,
context: strop_lsp::ReplyContext,
) {
if !self.lsp_context_fresh(&context) {
return;
}
let intent = crate::editor::io::OpenIntent::LspLocation {
context,
position: location.position,
};
match location.doc.filesystem {
Filesystem::Local => self.request_open(location.doc.path, intent),
Filesystem::Remote(endpoint) => {
// The target is a file on the replying server's host:
// resolve it through an open document's canonical seed
// (`with_path` keeps endpoint + native bytes) — the
// analogous local path is never opened or probed.
match self.remote_file_for(&endpoint) {
Some(seed) => match seed.with_path(location.doc.path.clone()) {
Ok(file) => self
.request_target(crate::files::FileTarget::Remote(file.into()), intent),
Err(error) => {
trace::services::rejected("lsp", "remote navigation target invalid");
self.message = format!("lsp: remote target invalid: {error}");
}
},
None => {
trace::services::rejected("lsp", "remote navigation endpoint lost");
self.message =
"lsp: the remote workspace for this target was closed".into();
}
}
}
Filesystem::Container(_) => {
trace::services::rejected("lsp", "navigation into a container namespace (unwired)");
self.message = "lsp: container locations are not navigable yet".into();
}
}
}
pub(crate) fn finish_lsp_jump(
&mut self,
target: strop_core::id::DocumentId,
position: strop_lsp::ServerPosition,
context: strop_lsp::ReplyContext,
) {
if !self.lsp_context_fresh(&context) {
trace::services::rejected("lsp", "navigation changed while target was loading");
return;
}
let Some(target_doc) = self.docs.get(target) else {
return;
};
let Some(binding) = self.lsp_state.bindings.get(&context.stamp.document) else {
return;
};
let outside = match &target_doc.source {
// A remote target is outside the workspace when its remote
// path leaves the binding's remote root — never by
// comparing against local paths.
crate::editor::document::DocumentSource::Remote(file) => {
!file.file.path().starts_with(&binding.root)
}
_ => target_doc
.buf
.path
.as_ref()
.is_some_and(|path| !self.cwd.join(path).starts_with(&binding.root)),
};
let line = position
.line
.get()
.min(target_doc.buf.len_lines().saturating_sub(1));
let text = target_doc
.buf
.text()
.byte_slice(target_doc.buf.line_start(line)..target_doc.buf.line_end(line));
let col = strop_lsp::to_byte_col_slice(text, position.column, context.encoding).get();
let head = target_doc
.buf
.clamp_boundary(target_doc.buf.line_start(line).saturating_add(col));
self.push_jump();
self.lsp_state.navigation = None;
self.switch_to(target);
if outside && !self.buf().readonly {
self.buf_mut().readonly = true;
self.message = "readonly — outside workspace (:set noro to edit)".into();
}
self.set_head(head);
self.clamp_cursor();
// definition/reference landings use the 0051 §7 placement:
// center the target unless it is already comfortably visible
self.place_jump_target();
// A server-originated jump carries its language-service context
// (0049 §4.1): the replying server keeps answering inside the
// target. A live binding another navigation established is never
// switched (0049 §4.5), and namespaces never cross (0049 §4.7).
let origin = self
.lsp_state
.bindings
.get(&context.stamp.document)
.map(|b| {
(
b.server,
b.root.clone(),
b.target.clone(),
b.language.clone(),
)
});
if let Some((server, root, origin_target, language)) = origin {
let target_bound = self.lsp_state.bindings.contains_key(&target);
let doc = self.lsp_doc_path(target);
// C and C++ headers are interchangeable for the server that
// serves both (0049 §4.4: an ambiguous `.h` inherits).
let language_compatible = doc
.as_ref()
.and_then(|doc| lsp_language(&doc.path))
.is_none_or(|known| {
known == language
|| (matches!(known, "c" | "cpp")
&& matches!(language.as_str(), "c" | "cpp"))
});
let context_free = !self.lsp_state.jump_contexts.contains_key(&target);
if let (false, true, Some(doc), true) =
(target_bound, context_free, doc, language_compatible)
{
if doc.filesystem == origin_target {
// A routing hint, not open state: didOpen follows in
// lsp_maybe_attach and becomes the real binding.
self.lsp_state.jump_contexts.insert(
target,
state::JumpContext {
server,
root,
language,
target: origin_target,
},
);
}
}
}
self.scroll_to_cursor(self.view_rows());
self.lsp_maybe_attach();
}
/// A local picker hit with a live LSP request context: the server
/// that produced the list owns the target's filesystem.
pub(crate) fn lsp_jump_from_picker(
&mut self,
path: PathBuf,
line: usize,
col: usize,
context: strop_lsp::ReplyContext,
) {
self.jump_to_location(
strop_lsp::ServerLocation {
doc: ResourceLocation::local(path),
position: strop_lsp::ServerPosition {
line: strop_core::id::LineIndex::new(line.saturating_sub(1)),
column: strop_lsp::ServerColumn::new(col.saturating_sub(1)),
},
},
context,
);
}
/// A remote picker hit (locations or diagnostics): re-parse the
/// endpoint and route through the endpoint's file identity — with
/// a live request context through the freshness-checked navigation
/// path (server columns), without one as a direct remote open at a
/// byte column. The analogous local path is never touched.
pub(crate) fn lsp_open_remote_hit(
&mut self,
endpoint: &strop_workspace::RemoteEndpoint,
path: &Path,
line: usize,
col: usize,
context: Option<strop_lsp::ReplyContext>,
) {
if let Some(context) = context {
self.jump_to_location(
strop_lsp::ServerLocation {
doc: ResourceLocation::remote(endpoint.clone(), path.to_owned()),
position: strop_lsp::ServerPosition {
line: strop_core::id::LineIndex::new(line.saturating_sub(1)),
column: strop_lsp::ServerColumn::new(col.saturating_sub(1)),
},
},
context,
);
} else if let Some(seed) = self.remote_file_for(endpoint) {
// Context-free remote hits (symbol rows) record too —
// ctrl-o after the jump returns (0047 §1).
self.push_jump();
match seed.with_path(path.to_owned()) {
Ok(file) => self.request_target(
crate::files::FileTarget::Remote(file.into()),
crate::editor::io::OpenIntent::Grep {
line: strop_core::id::LineIndex::new(line.saturating_sub(1)),
column: strop_core::id::ByteColumn::new(col.saturating_sub(1)),
},
),
Err(error) => self.message = format!("lsp remote location: {error}"),
}
} else {
self.message = "lsp: the remote workspace for this hit was closed".into();
}
}
}