Skip to content

Commit 4a841a2

Browse files
committed
Add Document::url for verbatim wire output
1 parent e807fd2 commit 4a841a2

8 files changed

Lines changed: 83 additions & 70 deletions

File tree

crates/ark/src/lsp/document.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,26 @@ use aether_lsp_utils::proto::PositionEncoding;
1111
use anyhow::anyhow;
1212
use oak_semantic::semantic_index::SemanticIndex;
1313
use tower_lsp::lsp_types;
14+
use tower_lsp::lsp_types::Url;
1415
use tree_sitter::Parser;
1516
use tree_sitter::Tree;
1617

1718
use crate::lsp::config::DocumentConfig;
1819

20+
/// Placeholder URL used by `Document::new`. Production paths replace
21+
/// this with the editor's verbatim URL via `WorldState::insert_document`.
22+
/// Tests that don't go through `WorldState` leave the placeholder.
23+
const PLACEHOLDER_URL: &str = "ark://internal/document-url-unset";
24+
1925
#[derive(Clone)]
2026
pub struct Document {
27+
/// The editor's URL for this document, byte-for-byte as received in
28+
/// `did_open`. Used for wire output (diagnostics, etc.) so the
29+
/// frontend always sees the URI it sent us, never a normalised
30+
/// round-trip. `WorldState::insert_document` populates this; the
31+
/// default constructor uses a placeholder.
32+
pub url: Url,
33+
2134
/// The document's textual contents.
2235
pub contents: String,
2336

@@ -76,6 +89,7 @@ impl Document {
7689
let line_index = biome_line_index::LineIndex::new(&contents);
7790

7891
Self {
92+
url: Url::parse(PLACEHOLDER_URL).expect("placeholder URL parses"),
7993
contents,
8094
version,
8195
ast,

crates/ark/src/lsp/goto_definition.rs

Lines changed: 24 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ mod parked_salsa_tests {
164164

165165
fn make_state(uri: &lsp_types::Url, doc: &Document) -> WorldState {
166166
let mut state = WorldState::default();
167-
state.documents.insert(uri.clone(), doc.clone());
167+
state.insert_document(uri.clone(), doc.clone());
168168
state
169169
}
170170

@@ -229,8 +229,8 @@ mod parked_salsa_tests {
229229
let pkg = Package::from_parts(pkg_root, desc, ns);
230230

231231
let mut state = WorldState::default();
232-
state.documents.insert(uri_aaa.clone(), doc_aaa.clone());
233-
state.documents.insert(uri_ccc.clone(), doc_ccc.clone());
232+
state.insert_document(uri_aaa.clone(), doc_aaa.clone());
233+
state.insert_document(uri_ccc.clone(), doc_ccc.clone());
234234
state.root = Some(SourceRoot::Package(pkg));
235235

236236
// ccc.R sees bbb.R's definition (later in collation)
@@ -283,9 +283,9 @@ mod parked_salsa_tests {
283283
let pkg = Package::from_parts(pkg_root, desc, ns);
284284

285285
let mut state = WorldState::default();
286-
state.documents.insert(uri_aaa.clone(), doc_aaa.clone());
287-
state.documents.insert(uri_bbb.clone(), doc_bbb);
288-
state.documents.insert(uri_ccc.clone(), doc_ccc.clone());
286+
state.insert_document(uri_aaa.clone(), doc_aaa.clone());
287+
state.insert_document(uri_bbb.clone(), doc_bbb);
288+
state.insert_document(uri_ccc.clone(), doc_ccc.clone());
289289
state.root = Some(SourceRoot::Package(pkg));
290290

291291
// aaa.R is now last in collation, so it can see bbb.R's definition
@@ -332,7 +332,7 @@ mod parked_salsa_tests {
332332
let pkg = Package::from_parts(pkg_root, desc, ns);
333333

334334
let mut state = WorldState::default();
335-
state.documents.insert(uri_aaa.clone(), doc_aaa.clone());
335+
state.insert_document(uri_aaa.clone(), doc_aaa.clone());
336336
state.root = Some(SourceRoot::Package(pkg));
337337

338338
// Cursor on `helper` inside the function body (line 0, col 16)
@@ -464,7 +464,7 @@ mod parked_salsa_tests {
464464
// `is.null` is NOT in the INDEX
465465
let doc_null = Document::new("is.null(1)\n", None);
466466
let uri_null = lsp_types::Url::from_file_path(pkg_root.join("R/bar.R")).unwrap();
467-
state.documents.insert(uri_null.clone(), doc_null.clone());
467+
state.insert_document(uri_null.clone(), doc_null.clone());
468468

469469
let params = make_params(uri_null, 0, 0);
470470
let result = goto_definition(&doc_null, params, &state).unwrap();
@@ -524,8 +524,7 @@ mod parked_salsa_tests {
524524

525525
let mut state = WorldState::default();
526526
state
527-
.documents
528-
.insert(script_uri.clone(), script_doc.clone());
527+
.insert_document(script_uri.clone(), script_doc.clone());
529528
state.workspace.folders = vec![lsp_types::Url::from_directory_path(dir.path()).unwrap()];
530529

531530
let params = make_params(script_uri, 1, 0);
@@ -551,10 +550,9 @@ mod parked_salsa_tests {
551550
let script_uri = lsp_types::Url::from_file_path(script_dir.join("script.R")).unwrap();
552551

553552
let mut state = WorldState::default();
554-
state.documents.insert(helpers_uri.clone(), helpers_doc);
553+
state.insert_document(helpers_uri.clone(), helpers_doc);
555554
state
556-
.documents
557-
.insert(script_uri.clone(), script_doc.clone());
555+
.insert_document(script_uri.clone(), script_doc.clone());
558556

559557
// Cursor on `helper` (line 1, col 0)
560558
let params = make_params(script_uri, 1, 0);
@@ -594,7 +592,7 @@ mod parked_salsa_tests {
594592

595593
let mut state = make_state(&script_uri, &script_doc);
596594
state.library = library;
597-
state.documents.insert(helpers_uri.clone(), helpers_doc);
595+
state.insert_document(helpers_uri.clone(), helpers_doc);
598596

599597
// `mutate` (line 1) resolves via dplyr, attached by helpers.R's library() call.
600598
// Package symbol, no NavigationTarget.
@@ -635,8 +633,7 @@ mod parked_salsa_tests {
635633

636634
let mut state = WorldState::default();
637635
state
638-
.documents
639-
.insert(script_uri.clone(), script_doc.clone());
636+
.insert_document(script_uri.clone(), script_doc.clone());
640637
// helpers.R is intentionally NOT inserted into state.documents
641638

642639
let params = make_params(script_uri, 1, 0);
@@ -756,8 +753,7 @@ mod parked_salsa_tests {
756753

757754
let mut state = WorldState::default();
758755
state
759-
.documents
760-
.insert(script_uri.clone(), script_doc.clone());
756+
.insert_document(script_uri.clone(), script_doc.clone());
761757

762758
// Should resolve without hanging. Both symbols are reachable
763759
// because a.R is visited first (gets its exports + b.R's exports),
@@ -802,8 +798,7 @@ mod parked_salsa_tests {
802798

803799
let mut state = WorldState::default();
804800
state
805-
.documents
806-
.insert(script_uri.clone(), script_doc.clone());
801+
.insert_document(script_uri.clone(), script_doc.clone());
807802

808803
let helpers_uri = lsp_types::Url::from_file_path(dir.path().join("helpers.R")).unwrap();
809804

@@ -849,8 +844,7 @@ mod parked_salsa_tests {
849844

850845
let mut state = WorldState::default();
851846
state
852-
.documents
853-
.insert(script_uri.clone(), script_doc.clone());
847+
.insert_document(script_uri.clone(), script_doc.clone());
854848

855849
// `foo` on line 2 should resolve to the LOCAL definition on line 1,
856850
// not to the sourced one from helpers.R.
@@ -898,8 +892,7 @@ mod parked_salsa_tests {
898892

899893
let mut state = WorldState::default();
900894
state
901-
.documents
902-
.insert(script_uri.clone(), script_doc.clone());
895+
.insert_document(script_uri.clone(), script_doc.clone());
903896

904897
let helpers_uri = lsp_types::Url::from_file_path(dir.path().join("helpers.R")).unwrap();
905898
let a_uri = lsp_types::Url::from_file_path(dir.path().join("a.R")).unwrap();
@@ -948,8 +941,7 @@ mod parked_salsa_tests {
948941

949942
let mut state = WorldState::default();
950943
state
951-
.documents
952-
.insert(script_uri.clone(), script_doc.clone());
944+
.insert_document(script_uri.clone(), script_doc.clone());
953945

954946
// `my_var` (line 2) should resolve to its own definition on line 1,
955947
// not to a Sourced duplicate.
@@ -994,8 +986,7 @@ mod parked_salsa_tests {
994986

995987
let mut state = WorldState::default();
996988
state
997-
.documents
998-
.insert(script_uri.clone(), script_doc.clone());
989+
.insert_document(script_uri.clone(), script_doc.clone());
999990

1000991
let (index, file_scope) = state.file_analysis(&script_uri, &script_doc);
1001992

@@ -1034,8 +1025,7 @@ mod parked_salsa_tests {
10341025

10351026
let mut state = WorldState::default();
10361027
state
1037-
.documents
1038-
.insert(script_uri.clone(), script_doc.clone());
1028+
.insert_document(script_uri.clone(), script_doc.clone());
10391029

10401030
let b_uri = lsp_types::Url::from_file_path(dir.path().join("b.R")).unwrap();
10411031

@@ -1064,8 +1054,7 @@ mod parked_salsa_tests {
10641054

10651055
let mut state = WorldState::default();
10661056
state
1067-
.documents
1068-
.insert(script_uri.clone(), script_doc.clone());
1057+
.insert_document(script_uri.clone(), script_doc.clone());
10691058

10701059
let helpers_uri = lsp_types::Url::from_file_path(dir.path().join("helpers.R")).unwrap();
10711060

@@ -1104,8 +1093,7 @@ mod parked_salsa_tests {
11041093

11051094
let mut state = WorldState::default();
11061095
state
1107-
.documents
1108-
.insert(script_uri.clone(), script_doc.clone());
1096+
.insert_document(script_uri.clone(), script_doc.clone());
11091097

11101098
let helpers_uri = lsp_types::Url::from_file_path(dir.path().join("helpers.R")).unwrap();
11111099

@@ -1139,8 +1127,7 @@ mod parked_salsa_tests {
11391127

11401128
let mut state = WorldState::default();
11411129
state
1142-
.documents
1143-
.insert(script_uri.clone(), script_doc.clone());
1130+
.insert_document(script_uri.clone(), script_doc.clone());
11441131

11451132
// `helper` on line 2 should resolve to helpers.R (source() shadows local)
11461133
let params = make_params(script_uri, 2, 0);
@@ -1178,8 +1165,7 @@ mod parked_salsa_tests {
11781165

11791166
let mut state = WorldState::default();
11801167
state
1181-
.documents
1182-
.insert(script_uri.clone(), script_doc.clone());
1168+
.insert_document(script_uri.clone(), script_doc.clone());
11831169

11841170
// `fn` on line 1 should resolve to the SECOND def in helpers.R (line 1)
11851171
let params = make_params(script_uri, 1, 0);

crates/ark/src/lsp/main_loop.rs

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -443,12 +443,7 @@ impl GlobalState {
443443
// time: a buffer may have opened or closed since the scan
444444
// kicked off. The buffer-drain inside `apply_scan_completed` uses
445445
// this set as its watcher-event `skip` argument.
446-
let editor_owned: HashSet<FilePath> = self.world
447-
.documents
448-
.keys()
449-
.map(FilePath::from_url)
450-
.collect();
451-
446+
let editor_owned: HashSet<FilePath> = self.world.documents.keys().cloned().collect();
452447
let followups = self.lsp_state.oak_scheduler.apply_scan_completed(
453448
&mut self.world.oak,
454449
scan,
@@ -989,7 +984,7 @@ async fn process_diagnostics_batch(batch: Vec<RefreshDiagnosticsTask>) {
989984
futures.push(task::spawn_blocking(move || {
990985
let _span = tracing::info_span!("diagnostics_refresh", uri = %uri).entered();
991986

992-
if let Some(document) = state.documents.get(&uri) {
987+
if let Some(document) = state.documents.get(&FilePath::from_url(&uri)) {
993988
// Special case testthat-specific behaviour. This is a simple
994989
// stopgap approach that has some false positives (e.g. when we
995990
// work on testthat itself the flag will always be true), but
@@ -1125,8 +1120,8 @@ pub(crate) fn diagnostics_refresh_all(state: WorldState) {
11251120
n = state.documents.len()
11261121
);
11271122

1128-
for (uri, _document) in state.documents.iter() {
1129-
if !ExtUrl::should_diagnose(uri) {
1123+
for document in state.documents.values() {
1124+
if !ExtUrl::should_diagnose(&document.url) {
11301125
continue;
11311126
}
11321127

@@ -1136,7 +1131,7 @@ pub(crate) fn diagnostics_refresh_all(state: WorldState) {
11361131
// non-oak state).
11371132
INDEXER_QUEUE
11381133
.send(IndexerQueueTask::Diagnostics(RefreshDiagnosticsTask {
1139-
uri: uri.clone(),
1134+
uri: document.url.clone(),
11401135
state: state.legacy_snapshot(),
11411136
}))
11421137
.unwrap_or_else(|err| lsp::log_error!("Failed to queue diagnostics refresh: {err}"));

crates/ark/src/lsp/state.rs

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::collections::HashMap;
22
use std::path::Path;
33

4+
use aether_path::FilePath;
45
use anyhow::anyhow;
56
use oak_db::OakDatabase;
67
use oak_semantic::library::Library;
@@ -15,8 +16,10 @@ use crate::lsp::inputs::source_root::SourceRoot;
1516
/// code. This is a pure value. There is no interior mutability in this data
1617
/// structure. It can be cloned and safely sent to other threads.
1718
pub(crate) struct WorldState {
18-
/// Watched documents
19-
pub(crate) documents: HashMap<Url, Document>,
19+
/// Watched documents, keyed on the normalised [`FilePath`] form.
20+
/// The verbatim editor URL is preserved on each [`Document::url`]
21+
/// for wire output.
22+
pub(crate) documents: HashMap<FilePath, Document>,
2023

2124
/// Watched folders
2225
pub(crate) workspace: Workspace,
@@ -79,15 +82,17 @@ impl WorldState {
7982
}
8083

8184
pub(crate) fn get_document(&self, uri: &Url) -> anyhow::Result<&Document> {
82-
if let Some(doc) = self.documents.get(uri) {
85+
let key = FilePath::from_url(uri);
86+
if let Some(doc) = self.documents.get(&key) {
8387
Ok(doc)
8488
} else {
8589
Err(anyhow!("Can't find document for URI {uri}"))
8690
}
8791
}
8892

8993
pub(crate) fn get_document_mut(&mut self, uri: &Url) -> anyhow::Result<&mut Document> {
90-
if let Some(doc) = self.documents.get_mut(uri) {
94+
let key = FilePath::from_url(uri);
95+
if let Some(doc) = self.documents.get_mut(&key) {
9196
Ok(doc)
9297
} else {
9398
Err(anyhow!("Can't find document for URI {uri}"))
@@ -115,6 +120,15 @@ impl WorldState {
115120
..self.clone()
116121
}
117122
}
123+
124+
/// Insert a document, keying on the normalised [`FilePath`] and
125+
/// stashing the verbatim editor URL on [`Document::url`] for wire
126+
/// output.
127+
pub(crate) fn insert_document(&mut self, uri: Url, mut doc: Document) {
128+
let key = FilePath::from_url(&uri);
129+
doc.url = uri;
130+
self.documents.insert(key, doc);
131+
}
118132
}
119133

120134
pub(crate) fn with_document<T, F>(
@@ -151,8 +165,11 @@ where
151165
}
152166

153167
pub(crate) fn workspace_uris(state: &WorldState) -> Vec<Url> {
154-
let uris: Vec<Url> = state.documents.iter().map(|elt| elt.0.clone()).collect();
155-
uris
168+
state
169+
.documents
170+
.values()
171+
.map(|doc| doc.url.clone())
172+
.collect()
156173
}
157174

158175
#[cfg(test)]

crates/ark/src/lsp/state_handlers.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ pub(crate) fn initialize(
143143
// Start first round of indexing. `state.documents` is empty at init since
144144
// no `didOpen` has fired yet, but build the set through the same shape we
145145
// use elsewhere so the call site reads consistently.
146-
let editor_owned: HashSet<FilePath> = state.documents.keys().map(FilePath::from_url).collect();
146+
let editor_owned: HashSet<FilePath> = state.documents.keys().cloned().collect();
147147
let pending = lsp_state.oak_scheduler.set_workspace_paths(
148148
&mut state.oak,
149149
&workspace_paths,
@@ -271,7 +271,7 @@ pub(crate) fn did_open(
271271
let document = Document::new_with_parser(contents, &mut parser, Some(version));
272272

273273
lsp_state.parsers.insert(uri.clone(), parser);
274-
state.documents.insert(uri.clone(), document.clone());
274+
state.insert_document(uri.clone(), document.clone());
275275

276276
let url_id = FilePath::from_url(&uri);
277277
state.oak.upsert_editor(url_id, contents.to_string());
@@ -330,7 +330,7 @@ pub(crate) fn did_close(
330330

331331
state
332332
.documents
333-
.remove(&uri)
333+
.remove(&FilePath::from_url(&uri))
334334
.ok_or(anyhow!("Failed to remove document for URI: {uri}"))?;
335335

336336
lsp_state
@@ -405,7 +405,7 @@ pub(crate) fn did_change_watched_files(
405405
) -> anyhow::Result<Vec<ScanRequest>> {
406406
// Editor owns the contents of files it has open: Oak should ignore
407407
// disk-side events for those URLs.
408-
let editor_owned: HashSet<FilePath> = state.documents.keys().map(FilePath::from_url).collect();
408+
let editor_owned: HashSet<FilePath> = state.documents.keys().cloned().collect();
409409

410410
let events: Vec<FileEvent> = params
411411
.changes
@@ -456,7 +456,7 @@ pub(crate) fn did_change_workspace_folders(
456456
// Editor-owned URLs survive eviction in `OrphanRoot` so the user's
457457
// open buffers keep getting analysed even when their workspace
458458
// folder goes away.
459-
let editor_owned: HashSet<FilePath> = state.documents.keys().map(FilePath::from_url).collect();
459+
let editor_owned: HashSet<FilePath> = state.documents.keys().cloned().collect();
460460

461461
let pending = lsp_state.oak_scheduler.set_workspace_paths(
462462
&mut state.oak,

0 commit comments

Comments
 (0)