Skip to content

Commit 89e1ecc

Browse files
authored
Consistently send verbatim URLs back to frontend (#1268)
Branched from #1267 Progress towards #1212 This PR straightens out the handing of verbatim wire URLs back to the frontend, so that our lexically normalised paths don't leak out (see #1250). - New `WorldState::wire_url()` method that resolves verbatim URLs for open files. The LSP layer calls it when communicating URLs back to the frontend, instead of the lexically-normalised paths that travel through the analysis layers. - The goto-definition, find-references, and rename handlers now use that method to create wire URLs. - `indexer::map()` now hands `File` instead of URLs, and callers that need a URL call `wire_url()`. Note that `ArkFile` still contains a copy of the verbatim URLs because Ark's diagnostics and roxygen handlers still use them. This is a temporary situation until these handlers are updated to the modern layering.
2 parents 59d0f7d + 5cee100 commit 89e1ecc

13 files changed

Lines changed: 123 additions & 36 deletions

File tree

crates/ark/src/lsp/completions/sources/composite/workspace.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,15 @@ fn completions_from_workspace(
6969
let token = token.as_str();
7070

7171
// get entries from the index
72-
indexer::map(&state.db, |uri, symbol, entry| {
72+
indexer::map(&state.db, |file, symbol, entry| {
7373
if !symbol.fuzzy_matches(token) {
7474
return;
7575
}
7676

7777
match &entry.data {
7878
indexer::IndexEntryData::Function { name, .. } => {
79+
let uri = state.wire_url(file);
80+
7981
let fun_context = match completion_context.function_context() {
8082
Ok(fun_context) => fun_context,
8183
Err(err) => {

crates/ark/src/lsp/diagnostics.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ pub(crate) fn generate_diagnostics(
171171
context.document_symbols.push(HashMap::new());
172172

173173
// Add the current workspace symbols.
174-
indexer::map(db, |_uri, _symbol, entry| match &entry.data {
174+
indexer::map(db, |_file, _symbol, entry| match &entry.data {
175175
indexer::IndexEntryData::Function { name, arguments: _ } => {
176176
context.workspace_symbols.insert(name.to_string());
177177
},

crates/ark/src/lsp/find_references.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ pub(crate) fn find_references(
3030
.iter()
3131
.filter_map(|fr| {
3232
let range = to_proto::range(fr.range, fr.file.line_index(db), encoding).log_err()?;
33-
Some(Location::new(fr.file.path(db).to_url(), range))
33+
Some(Location::new(state.wire_url(fr.file), range))
3434
})
3535
.collect();
3636

crates/ark/src/lsp/goto_definition.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ pub(crate) fn goto_definition(
3535
// to several bindings; the client offers all of them.
3636
let links = targets
3737
.iter()
38-
.map(|target| nav_target_to_link(db, encoding, target))
38+
.map(|target| nav_target_to_link(state, encoding, target))
3939
.collect::<anyhow::Result<Vec<_>>>()?;
4040

4141
Ok(Some(GotoDefinitionResponse::Link(links)))
@@ -45,17 +45,18 @@ pub(crate) fn goto_definition(
4545
/// offsets in the target file, so we translate them through that file's own
4646
/// line index, not the file the request came from.
4747
fn nav_target_to_link(
48-
db: &dyn Db,
48+
state: &WorldState,
4949
encoding: PositionEncoding,
5050
target: &NavigationTarget,
5151
) -> anyhow::Result<LocationLink> {
52+
let db = &state.db;
5253
let line_index = target.file.line_index(db);
5354
let target_range = to_proto::range(target.full_range, line_index, encoding)?;
5455
let target_selection_range = to_proto::range(target.focus_range, line_index, encoding)?;
5556

5657
Ok(LocationLink {
5758
origin_selection_range: None,
58-
target_uri: target.file.path(db).to_url(),
59+
target_uri: state.wire_url(target.file),
5960
target_range,
6061
target_selection_range,
6162
})

crates/ark/src/lsp/indexer.rs

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ use std::sync::LazyLock;
1010

1111
use aether_lsp_utils::proto::to_proto;
1212
use aether_lsp_utils::proto::PositionEncoding;
13-
use aether_path::FilePath;
1413
use oak_db::File;
1514
use regex::Regex;
1615
use stdext::result::ResultExt;
@@ -19,7 +18,6 @@ use stdext::unwrap::IntoResult;
1918
use tower_lsp::lsp_types::Range;
2019
use tree_sitter::Node;
2120
use tree_sitter::Query;
22-
use url::Url;
2321

2422
use crate::lsp;
2523
use crate::lsp::db::ArkDb;
@@ -82,16 +80,14 @@ pub struct IndexEntry {
8280
}
8381

8482
/// Convert an index entry's tree-sitter point range to an LSP range, resolving
85-
/// the file's line index from the db. Returns `None` if the file is no longer
86-
/// in the db or the points fall outside it, in which case the symbol is
87-
/// dropped from the results.
83+
/// the file's line index from the db. Returns `None` if the points fall outside
84+
/// the line index, in which case the symbol is dropped from the results.
8885
pub(crate) fn index_range_to_lsp_range(
8986
db: &dyn ArkDb,
90-
uri: &Url,
87+
file: File,
9188
range: IndexRange,
9289
encoding: PositionEncoding,
9390
) -> Option<Range> {
94-
let file = db.file_by_path(&FilePath::from_url(uri))?;
9591
let line_index = file.line_index(db);
9692

9793
let to_position = |point: IndexPoint| {
@@ -160,15 +156,16 @@ fn file_index(db: &dyn ArkDb, file: File) -> FileIndex {
160156
FileIndex { symbols }
161157
}
162158

163-
/// Visit every workspace symbol across all indexable files.
164-
pub(crate) fn map(db: &dyn ArkDb, mut callback: impl FnMut(&Url, &str, &IndexEntry)) {
159+
/// Visit every workspace symbol across all indexable files. Callers that need a
160+
/// URL for messages to the frontend can resolve it from `File` via
161+
/// `WorldState::wire_url`.
162+
pub(crate) fn map(db: &dyn ArkDb, mut callback: impl FnMut(File, &str, &IndexEntry)) {
165163
for &file in oak_db::workspace_files(db) {
166164
if !is_indexable(db, file) {
167165
continue;
168166
}
169-
let url = file.path(db).to_url();
170167
for (symbol, entry) in file_index(db, file).symbols.iter() {
171-
callback(&url, symbol, entry);
168+
callback(file, symbol, entry);
172169
}
173170
}
174171
}
@@ -380,6 +377,7 @@ mod tests {
380377
use assert_matches::assert_matches;
381378
use insta::assert_debug_snapshot;
382379
use oak_scan::DbScan;
380+
use url::Url;
383381

384382
use super::*;
385383
use crate::lsp::ark_file::test_ark_file;

crates/ark/src/lsp/rename.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ pub(crate) fn rename(
6666
let mut changes: HashMap<lsp_types::Url, Vec<TextEdit>> = HashMap::new();
6767
for range in ranges {
6868
let line_index = range.file.line_index(db);
69-
let target_url = range.file.path(db).to_url();
69+
let target_url = state.wire_url(range.file);
7070
let range = to_proto::range(range.range, line_index, encoding)?;
7171
changes.entry(target_url).or_default().push(TextEdit {
7272
range,

crates/ark/src/lsp/state.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,18 @@ impl WorldState {
120120
Ok(ark_file.clone())
121121
}
122122

123+
/// URL to put on the wire for `file`. Open buffers keep the editor's
124+
/// verbatim URL so the frontend sees the URI it sent us. Files that were
125+
/// never opened in the editor (disk-scanned files, resolution targets) have
126+
/// no verbatim URL, so synthesise one from the normalised path.
127+
pub(crate) fn wire_url(&self, file: File) -> Url {
128+
let path = file.path(&self.db);
129+
self.open_files
130+
.get(path)
131+
.map(|ark_file| ark_file.url.clone())
132+
.unwrap_or_else(|| path.to_url())
133+
}
134+
123135
/// Register an editor buffer in `open_files`, keying on the normalised
124136
/// [`FilePath`] and stashing the verbatim editor URL on [`ArkFile::url`] for
125137
/// wire output.

crates/ark/src/lsp/symbols.rs

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -64,24 +64,23 @@ pub(crate) fn symbols(
6464
let encoding = state.config.position_encoding;
6565
let mut info: Vec<SymbolInformation> = Vec::new();
6666

67-
indexer::map(db, |uri, symbol, entry| {
67+
indexer::map(db, |file, symbol, entry| {
6868
if !symbol.fuzzy_matches(query) {
6969
return;
7070
}
7171

72-
let Some(range) = indexer::index_range_to_lsp_range(db, uri, entry.range, encoding) else {
72+
let Some(range) = indexer::index_range_to_lsp_range(db, file, entry.range, encoding) else {
7373
return;
7474
};
7575

76+
let uri = state.wire_url(file);
77+
7678
match &entry.data {
7779
IndexEntryData::Function { name, arguments: _ } => {
7880
info.push(SymbolInformation {
7981
name: name.to_string(),
8082
kind: SymbolKind::FUNCTION,
81-
location: Location {
82-
uri: uri.clone(),
83-
range,
84-
},
83+
location: Location { uri, range },
8584
tags: None,
8685
deprecated: None,
8786
container_name: None,
@@ -93,10 +92,7 @@ pub(crate) fn symbols(
9392
info.push(SymbolInformation {
9493
name: title.to_string(),
9594
kind: SymbolKind::STRING,
96-
location: Location {
97-
uri: uri.clone(),
98-
range,
99-
},
95+
location: Location { uri, range },
10096
tags: None,
10197
deprecated: None,
10298
container_name: None,
@@ -108,10 +104,7 @@ pub(crate) fn symbols(
108104
info.push(SymbolInformation {
109105
name: name.clone(),
110106
kind: SymbolKind::VARIABLE,
111-
location: Location {
112-
uri: uri.clone(),
113-
range,
114-
},
107+
location: Location { uri, range },
115108
tags: None,
116109
deprecated: None,
117110
container_name: None,
@@ -122,10 +115,7 @@ pub(crate) fn symbols(
122115
info.push(SymbolInformation {
123116
name: name.clone(),
124117
kind: SymbolKind::METHOD,
125-
location: Location {
126-
uri: uri.clone(),
127-
range,
128-
},
118+
location: Location { uri, range },
129119
tags: None,
130120
deprecated: None,
131121
container_name: None,

crates/ark/src/lsp/tests.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ mod find_references;
44
mod goto_definition;
55
mod main_loop;
66
mod rename;
7+
mod state;
78
mod state_handlers;
89
mod utils;

crates/ark/src/lsp/tests/find_references.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use aether_path::FilePath;
12
use tower_lsp::lsp_types;
23
use tower_lsp::lsp_types::Location;
34
use tower_lsp::lsp_types::ReferenceContext;
@@ -170,6 +171,24 @@ fn test_cross_file_dollar_kind() {
170171
.any(|l| l.uri == uri2 && l.range == range((2, 0), (2, 3))));
171172
}
172173

174+
#[test]
175+
fn test_locations_use_verbatim_url() {
176+
// The doubled slash survives in the editor's URL but `FilePath` normalises
177+
// it away, so a path round-trip would change it. Locations must carry the
178+
// exact URI the editor opened the buffer with.
179+
let code = "foo <- 1\nfoo\n";
180+
let uri = lsp_types::Url::parse("file:///C:/proj//foo.R").unwrap();
181+
let state = make_state(&uri, code);
182+
183+
let params = make_params(uri.clone(), 1, 0, true);
184+
let locs = find_references(params, &state).unwrap();
185+
186+
assert!(!locs.is_empty());
187+
assert!(locs.iter().all(|loc| loc.uri == uri));
188+
// Confirm the round-trip really would have differed, so the check above bites.
189+
assert_ne!(FilePath::from_url(&uri).to_url(), uri);
190+
}
191+
173192
#[test]
174193
fn test_cross_file_namespace_access() {
175194
// Cursor on `mutate` in `dplyr::mutate` -- structural scan matches every

0 commit comments

Comments
 (0)