From 5cb59dfdab92fdda633846405a755633c960ddce Mon Sep 17 00:00:00 2001 From: Isaac Clayton Date: Thu, 7 Jul 2022 18:14:16 +0200 Subject: [PATCH] Fix errors resulting from rebase --- Cargo.lock | 109 +- crates/language/src/language.rs | 2 +- crates/plugin_runtime/Cargo.toml | 8 +- crates/project/src/lsp_command.rs | 4 +- crates/project/src/project.rs | 3078 +-------------------------- crates/project/src/project_tests.rs | 42 +- 6 files changed, 140 insertions(+), 3103 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3c17a9d8b8..f3df25ce58 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1183,23 +1183,24 @@ dependencies = [ [[package]] name = "cranelift-bforest" -version = "0.84.0" +version = "0.85.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fa7c3188913c2d11a361e0431e135742372a2709a99b103e79758e11a0a797e" +checksum = "7901fbba05decc537080b07cb3f1cadf53be7b7602ca8255786288a8692ae29a" dependencies = [ "cranelift-entity", ] [[package]] name = "cranelift-codegen" -version = "0.84.0" +version = "0.85.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29285f70fd396a8f64455a15a6e1d390322e4a5f5186de513141313211b0a23e" +checksum = "37ba1b45d243a4a28e12d26cd5f2507da74e77c45927d40de8b6ffbf088b46b5" dependencies = [ "cranelift-bforest", "cranelift-codegen-meta", "cranelift-codegen-shared", "cranelift-entity", + "cranelift-isle", "gimli", "log", "regalloc2", @@ -1209,33 +1210,33 @@ dependencies = [ [[package]] name = "cranelift-codegen-meta" -version = "0.84.0" +version = "0.85.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057eac2f202ec95aebfd8d495e88560ac085f6a415b3c6c28529dc5eb116a141" +checksum = "54cc30032171bf230ce22b99c07c3a1de1221cb5375bd6dbe6dbe77d0eed743c" dependencies = [ "cranelift-codegen-shared", ] [[package]] name = "cranelift-codegen-shared" -version = "0.84.0" +version = "0.85.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75d93869efd18874a9341cfd8ad66bcb08164e86357a694a0e939d29e87410b9" +checksum = "a23f2672426d2bb4c9c3ef53e023076cfc4d8922f0eeaebaf372c92fae8b5c69" [[package]] name = "cranelift-entity" -version = "0.84.0" +version = "0.85.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e34bd7a1fefa902c90a921b36323f17a398b788fa56a75f07a29d83b6e28808" +checksum = "886c59a5e0de1f06dbb7da80db149c75de10d5e2caca07cdd9fef8a5918a6336" dependencies = [ "serde", ] [[package]] name = "cranelift-frontend" -version = "0.84.0" +version = "0.85.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "457018dd2d6ee300953978f63215b5edf3ae42dbdf8c7c038972f10394599f72" +checksum = "ace74eeca11c439a9d4ed1a5cb9df31a54cd0f7fbddf82c8ce4ea8e9ad2a8fe0" dependencies = [ "cranelift-codegen", "log", @@ -1244,10 +1245,16 @@ dependencies = [ ] [[package]] -name = "cranelift-native" -version = "0.84.0" +name = "cranelift-isle" +version = "0.85.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bba027cc41bf1d0eee2ddf16caba2ee1be682d0214520fff0129d2c6557fda89" +checksum = "db1ae52a5cc2cad0d86fdd3dcb16b7217d2f1e65ab4f5814aa4f014ad335fa43" + +[[package]] +name = "cranelift-native" +version = "0.85.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dadcfb7852900780d37102bce5698bcd401736403f07b52e714ff7a180e0e22f" dependencies = [ "cranelift-codegen", "libc", @@ -1256,9 +1263,9 @@ dependencies = [ [[package]] name = "cranelift-wasm" -version = "0.84.0" +version = "0.85.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b17639ced10b9916c9be120d38c872ea4f9888aa09248568b10056ef0559bfa" +checksum = "c84e3410960389110b88f97776f39f6d2c8becdaa4cd59e390e6b76d9d0e7190" dependencies = [ "cranelift-codegen", "cranelift-entity", @@ -4181,9 +4188,9 @@ dependencies = [ [[package]] name = "regalloc2" -version = "0.1.3" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "904196c12c9f55d3aea578613219f493ced8e05b3d0c6a42d11cb4142d8b4879" +checksum = "4a8d23b35d7177df3b9d31ed8a9ab4bf625c668be77a319d4f5efd4a5257701c" dependencies = [ "fxhash", "log", @@ -6314,9 +6321,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasi-cap-std-sync" -version = "0.37.0" +version = "0.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1029972e08194fe0ca67a83221945fff9d6d1b0dd8b752c6073b45d0254ac71b" +checksum = "c1c4e73ed64b92ae87b416f4274b3c827180b02b67f835f66a86fc4267b77349" dependencies = [ "anyhow", "async-trait", @@ -6338,9 +6345,9 @@ dependencies = [ [[package]] name = "wasi-common" -version = "0.37.0" +version = "0.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "396cc8d920f924474f589f6a2202ad9783579ef12f96e6b675d0b2f3917c7126" +checksum = "cc983eb93607a61f64152ec8728bf453f4dfdf22e7ab1784faac3297fe9a035e" dependencies = [ "anyhow", "bitflags", @@ -6431,18 +6438,18 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.84.0" +version = "0.85.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77dc97c22bb5ce49a47b745bed8812d30206eff5ef3af31424f2c1820c0974b2" +checksum = "570460c58b21e9150d2df0eaaedbb7816c34bcec009ae0dcc976e40ba81463e7" dependencies = [ "indexmap", ] [[package]] name = "wasmtime" -version = "0.37.0" +version = "0.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfdd1101bdfa0414a19018ec0a091951a20b695d4d04f858d49f6c4cc53cd8dd" +checksum = "e76e2b2833bb0ece666ccdbed7b71b617d447da11f1bb61f4f2bab2648f745ee" dependencies = [ "anyhow", "async-trait", @@ -6474,9 +6481,9 @@ dependencies = [ [[package]] name = "wasmtime-cache" -version = "0.37.0" +version = "0.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79da81ed0724392948ad7a0fb5088ff1bd15fa937356c8c037c6b1c8b5473cde" +checksum = "743a9f142d93318262d7e1fe329394ff2e8f86a1df45ae5e4f0eedba215ca5ce" dependencies = [ "anyhow", "base64 0.13.0", @@ -6494,9 +6501,9 @@ dependencies = [ [[package]] name = "wasmtime-cranelift" -version = "0.37.0" +version = "0.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16e78edcfb0daa9a9579ac379d00e2d5a5b2a60c0d653c8c95e8412f2166acb9" +checksum = "5dc0f80afa1ce97083a7168e6b6948d015d6237369e9f4a511d38c9c4ac8fbb9" dependencies = [ "anyhow", "cranelift-codegen", @@ -6516,9 +6523,9 @@ dependencies = [ [[package]] name = "wasmtime-environ" -version = "0.37.0" +version = "0.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4201389132ec467981980549574b33fc70d493b40f2c045c8ce5c7b54fbad97e" +checksum = "0816d9365196f1f447060087e0f87239ccded830bd54970a1168b0c9c8e824c9" dependencies = [ "anyhow", "cranelift-entity", @@ -6536,9 +6543,9 @@ dependencies = [ [[package]] name = "wasmtime-fiber" -version = "0.37.0" +version = "0.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ba6777a84b44f9a384b5c9d511ae3d86534438b7e25d928b8e8e858ecad5df2" +checksum = "715afdb87a3bcf1eae3f098c742d650fb783abdb8a7ca87076ea1cabecabea5d" dependencies = [ "cc", "rustix", @@ -6547,9 +6554,9 @@ dependencies = [ [[package]] name = "wasmtime-jit" -version = "0.37.0" +version = "0.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1587ca7752d00862faa540d00fd28e5ccf1ac61ba19756449193f1153cb2b127" +checksum = "5c687f33cfa0f89ec1646929d0ff102087052cf9f0d15533de56526b0da0d1b3" dependencies = [ "addr2line", "anyhow", @@ -6574,9 +6581,9 @@ dependencies = [ [[package]] name = "wasmtime-jit-debug" -version = "0.37.0" +version = "0.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b27233ab6c8934b23171c64f215f902ef19d18c1712b46a0674286d1ef28d5dd" +checksum = "b252d1d025f94f3954ba2111f12f3a22826a0764a11c150c2d46623115a69e27" dependencies = [ "lazy_static", "object", @@ -6585,9 +6592,9 @@ dependencies = [ [[package]] name = "wasmtime-runtime" -version = "0.37.0" +version = "0.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47d3b0b8f13db47db59d616e498fe45295819d04a55f9921af29561827bdb816" +checksum = "ace251693103c9facbbd7df87a29a75e68016e48bc83c09133f2fda6b575e0ab" dependencies = [ "anyhow", "backtrace", @@ -6612,9 +6619,9 @@ dependencies = [ [[package]] name = "wasmtime-types" -version = "0.37.0" +version = "0.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1630d9dca185299bec7f557a7e73b28742fe5590caf19df001422282a0a98ad1" +checksum = "d129b0487a95986692af8708ffde9c50b0568dcefd79200941d475713b4f40bb" dependencies = [ "cranelift-entity", "serde", @@ -6624,9 +6631,9 @@ dependencies = [ [[package]] name = "wasmtime-wasi" -version = "0.37.0" +version = "0.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d507b07263a4923440da662c611affc2e671714bb6e5f68f6b068a5736bcd7f" +checksum = "fb49791530b3a3375897a6d5a8bfa9914101ef8a672d01c951e70b46fd953c15" dependencies = [ "anyhow", "wasi-cap-std-sync", @@ -6751,9 +6758,9 @@ dependencies = [ [[package]] name = "wiggle" -version = "0.37.0" +version = "0.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799e618e90d9f3d6d76943d9f2903c609b62f2ab5d16dedcff4816d38db726dd" +checksum = "91c38020359fabec5e5ce5a3f667af72e9a203bc6fe8caeb8931d3a870754d9d" dependencies = [ "anyhow", "async-trait", @@ -6766,9 +6773,9 @@ dependencies = [ [[package]] name = "wiggle-generate" -version = "0.37.0" +version = "0.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534d70579cd9aca243e94eeb14a348dbc9ae87e7758ffebfdd4bb4a98d59b9b0" +checksum = "adc4e4420b496b04920ae3e41424029aba95c15a5e2e2b4012d14ec83770a3ef" dependencies = [ "anyhow", "heck 0.4.0", @@ -6781,9 +6788,9 @@ dependencies = [ [[package]] name = "wiggle-macro" -version = "0.37.0" +version = "0.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87216b1f58eeee44a6385de39e8c03190c209942cfa34344c9ba69426f146d8d" +checksum = "2e541a0be1f2c4d53471d8a9df81c2d8725a3f023d8259f555c65b03d515aaab" dependencies = [ "proc-macro2", "quote", diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 66b52629db..572bb8c8ae 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -541,7 +541,7 @@ async fn fetch_latest_server_binary_path( .broadcast((language.clone(), LanguageServerBinaryStatus::Downloading)) .await?; let path = adapter - .fetch_server_binary(version_info, http_client, container_dir.clone()) + .fetch_server_binary(version_info, http_client, container_dir.to_path_buf()) .await?; lsp_binary_statuses_tx .broadcast((language.clone(), LanguageServerBinaryStatus::Downloaded)) diff --git a/crates/plugin_runtime/Cargo.toml b/crates/plugin_runtime/Cargo.toml index 87a93fb1f4..21c1ab973d 100644 --- a/crates/plugin_runtime/Cargo.toml +++ b/crates/plugin_runtime/Cargo.toml @@ -4,9 +4,9 @@ version = "0.1.0" edition = "2021" [dependencies] -wasmtime = "0.37.0" -wasmtime-wasi = "0.37.0" -wasi-common = "0.37.0" +wasmtime = "0.38" +wasmtime-wasi = "0.38" +wasi-common = "0.38" anyhow = { version = "1.0", features = ["std"] } serde = "1.0" serde_json = "1.0" @@ -15,4 +15,4 @@ pollster = "0.2.5" smol = "1.2.5" [build-dependencies] -wasmtime = "0.38.1" +wasmtime = "0.38" diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 212edb9219..0c30ee2924 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -389,7 +389,7 @@ impl LspCommand for GetDefinition { this.open_local_buffer_via_lsp( target_uri, language_server.server_id(), - lsp_adapter.name(), + lsp_adapter.name.clone(), cx, ) }) @@ -610,7 +610,7 @@ impl LspCommand for GetReferences { this.open_local_buffer_via_lsp( lsp_location.uri, language_server.server_id(), - lsp_adapter.name(), + lsp_adapter.name.clone(), cx, ) }) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 160f66eeb5..7adab020b5 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -199,7 +199,7 @@ pub enum Event { pub enum LanguageServerState { Starting(Task>>), Running { - adapter: Arc, + adapter: Arc, server: Arc, }, } @@ -708,7 +708,7 @@ impl Project { }) } - fn on_settings_changed(&mut self, cx: &mut ModelContext<'_, Self>) { + fn on_settings_changed(&mut self, cx: &mut ModelContext) { let settings = cx.global::(); let mut language_servers_to_start = Vec::new(); @@ -734,7 +734,7 @@ impl Project { if let Some(lsp_adapter) = language.lsp_adapter() { if !settings.enable_language_server(Some(&language.name())) { let lsp_name = &lsp_adapter.name; - for (worktree_id, started_lsp_name) in self.language_servers.keys() { + for (worktree_id, started_lsp_name) in self.language_server_ids.keys() { if lsp_name == started_lsp_name { language_servers_to_stop.push((*worktree_id, started_lsp_name.clone())); } @@ -1627,6 +1627,7 @@ impl Project { }) } + /// LanguageServerName is owned, because it is inserted into a map fn open_local_buffer_via_lsp( &mut self, abs_path: lsp::Url, @@ -1648,10 +1649,11 @@ impl Project { this.create_local_worktree(&abs_path, false, cx) }) .await?; - let name = lsp_adapter.name.clone(); this.update(&mut cx, |this, cx| { - this.language_servers - .insert((worktree.read(cx).id(), name), (lsp_adapter, lsp_server)); + this.language_server_ids.insert( + (worktree.read(cx).id(), language_server_name), + language_server_id, + ); }); (worktree, PathBuf::new()) }; @@ -2002,9 +2004,10 @@ impl Project { fn language_servers_for_worktree( &self, worktree_id: WorktreeId, - ) -> impl Iterator, Arc)> { - self.language_servers.iter().filter_map( - move |((language_server_worktree_id, _), server)| { + ) -> impl Iterator, &Arc)> { + self.language_server_ids + .iter() + .filter_map(move |((language_server_worktree_id, _), id)| { if *language_server_worktree_id == worktree_id { if let Some(LanguageServerState::Running { adapter, server }) = self.language_servers.get(&id) @@ -2042,7 +2045,7 @@ impl Project { worktree_id: WorktreeId, worktree_path: Arc, language: Arc, - cx: &mut ModelContext<'_, Self>, + cx: &mut ModelContext, ) { if !cx .global::() @@ -2056,8 +2059,8 @@ impl Project { } else { return; }; + let key = (worktree_id, adapter.name.clone()); - let key = (worktree_id, adapter.name); self.language_server_ids .entry(key.clone()) .or_insert_with(|| { @@ -2069,18 +2072,15 @@ impl Project { self.client.http_client(), cx, ); - self.language_servers.insert( server_id, LanguageServerState::Starting(cx.spawn_weak(|this, mut cx| async move { let language_server = language_server?.await.log_err()?; let language_server = language_server - .initialize(adapter.initialization_options()) + .initialize(adapter.initialization_options.clone()) .await .log_err()?; let this = this.upgrade(&cx)?; - let disk_based_diagnostics_progress_token = - adapter.disk_based_diagnostics_progress_token(); language_server .on_notification::({ @@ -2089,10 +2089,9 @@ impl Project { move |params, mut cx| { if let Some(this) = this.upgrade(&cx) { this.update(&mut cx, |this, cx| { - // TODO(isaac): remove block on - smol::block_on(this.on_lsp_diagnostics_published( + this.on_lsp_diagnostics_published( server_id, params, &adapter, cx, - )) + ); }); } } @@ -2170,11 +2169,13 @@ impl Project { language_server.clone(), cx, ) - } }) .detach(); + let disk_based_diagnostics_progress_token = + adapter.disk_based_diagnostics_progress_token.clone(); + language_server .on_notification::({ let this = this.downgrade(); @@ -2184,7 +2185,7 @@ impl Project { this.on_lsp_progress( params, server_id, - disk_based_diagnostics_progress_token, + disk_based_diagnostics_progress_token.clone(), cx, ); }); @@ -2258,7 +2259,7 @@ impl Project { continue; }; if file.worktree.read(cx).id() != key.0 - || language.lsp_adapter().map(|a| a.name()) + || language.lsp_adapter().map(|a| a.name.clone()) != Some(key.1.clone()) { continue; @@ -2271,14 +2272,15 @@ impl Project { .or_insert_with(|| vec![(0, buffer.text_snapshot())]); let (version, initial_snapshot) = versions.last().unwrap(); let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap(); - let language_id = - adapter.id_for_language(language.name().as_ref()); language_server .notify::( lsp::DidOpenTextDocumentParams { text_document: lsp::TextDocumentItem::new( uri, - language_id.unwrap_or_default(), + adapter + .id_for_language + .clone() + .unwrap_or_default(), *version, initial_snapshot.text(), ), @@ -2306,6 +2308,8 @@ impl Project { }) })), ); + + server_id }); } @@ -2394,7 +2398,7 @@ impl Project { worktree_id: WorktreeId, fallback_path: Arc, language: Arc, - cx: &mut ModelContext<'_, Self>, + cx: &mut ModelContext, ) { let adapter = if let Some(adapter) = language.lsp_adapter() { adapter @@ -2402,7 +2406,8 @@ impl Project { return; }; - let stop = self.stop_language_server(worktree_id, adapter.name.clone(), cx); + let server_name = adapter.name.clone(); + let stop = self.stop_language_server(worktree_id, server_name.clone(), cx); cx.spawn_weak(|this, mut cx| async move { let (original_root_path, orphaned_worktrees) = stop.await; if let Some(this) = this.upgrade(&cx) { @@ -2455,7 +2460,7 @@ impl Project { &mut self, progress: lsp::ProgressParams, server_id: usize, - disk_based_diagnostics_progress_token: Option<&str>, + disk_based_diagnostics_progress_token: Option, cx: &mut ModelContext, ) { let token = match progress.token { @@ -2479,7 +2484,8 @@ impl Project { return; } - let same_token = Some(token.as_ref()) == disk_based_diagnostics_progress_token; + let same_token = + Some(token.as_ref()) == disk_based_diagnostics_progress_token.as_ref().map(|x| &**x); match progress { lsp::WorkDoneProgress::Begin(report) => { @@ -3533,50 +3539,17 @@ impl Project { { continue; } - let (old_range, mut new_text) = match lsp_completion.text_edit.as_ref() - { - // If the language server provides a range to overwrite, then - // check that the range is valid. - Some(lsp::CompletionTextEdit::Edit(edit)) => { - let range = range_from_lsp(edit.range); - let start = snapshot.clip_point_utf16(range.start, Bias::Left); - let end = snapshot.clip_point_utf16(range.end, Bias::Left); - if start != range.start || end != range.end { - log::info!("completion out of expected range"); - return None; - } - ( - snapshot.anchor_before(start)..snapshot.anchor_after(end), - edit.new_text.clone(), - ) - } - // If the language server does not provide a range, then infer - // the range based on the syntax tree. - None => { - if position != clipped_position { - log::info!("completion out of expected range"); - return None; - } - let Range { start, end } = range_for_token - .get_or_insert_with(|| { - let offset = position.to_offset(&snapshot); - let (range, kind) = snapshot.surrounding_word(offset); - if kind == Some(CharKind::Word) { - range - } else { - offset..offset - } - }) - .clone(); - let text = lsp_completion - .insert_text - .as_ref() - .unwrap_or(&lsp_completion.label) - .clone(); - ( - snapshot.anchor_before(start)..snapshot.anchor_after(end), - text.clone(), - ) + + let (old_range, new_text) = match lsp_completion.text_edit.as_ref() { + // If the language server provides a range to overwrite, then + // check that the range is valid. + Some(lsp::CompletionTextEdit::Edit(edit)) => { + let range = range_from_lsp(edit.range); + let start = snapshot.clip_point_utf16(range.start, Bias::Left); + let end = snapshot.clip_point_utf16(range.end, Bias::Left); + if start != range.start || end != range.end { + log::info!("completion out of expected range"); + continue; } ( snapshot.anchor_before(start)..snapshot.anchor_after(end), @@ -3590,22 +3563,6 @@ impl Project { log::info!("completion out of expected range"); continue; } - }; - - LineEnding::normalize(&mut new_text); - Some(Completion { - old_range, - new_text, - label: { - match language.as_ref() { - Some(l) => l.label_for_completion(&lsp_completion).await, - None => None, - } - .unwrap_or_else(|| { - CodeLabel::plain( - lsp_completion.label.clone(), - lsp_completion.filter_text.as_deref(), - ) let Range { start, end } = range_for_token .get_or_insert_with(|| { let offset = position.to_offset(&snapshot); @@ -3690,13 +3647,14 @@ impl Project { }) .await; - let mut completions = Vec::new(); + let mut result = Vec::new(); for completion in response.completions.into_iter() { - completions.push( - language::proto::deserialize_completion(completion, language.clone()).await, - ); + let completion = + language::proto::deserialize_completion(completion, language.clone()) + .await?; + result.push(completion); } - completions.into_iter().collect() + Ok(result) }) } else { Task::ready(Ok(Default::default())) @@ -4066,7 +4024,7 @@ impl Project { this.open_local_buffer_via_lsp( op.text_document.uri, language_server.server_id(), - lsp_adapter.name(), + lsp_adapter.name.clone(), cx, ) }) @@ -5998,11 +5956,11 @@ impl Project { &self, buffer: &Buffer, cx: &AppContext, - ) -> Option<&(Arc, Arc)> { + ) -> Option<(&Arc, &Arc)> { if let Some((file, language)) = File::from_dyn(buffer.file()).zip(buffer.language()) { let name = language.lsp_adapter()?.name.clone(); let worktree_id = file.worktree_id(cx); - let key = (worktree_id, language.lsp_adapter()?.name()); + let key = (worktree_id, name); if let Some(server_id) = self.language_server_ids.get(&key) { if let Some(LanguageServerState::Running { adapter, server }) = @@ -6302,2931 +6260,3 @@ impl Item for Buffer { File::from_dyn(self.file()).and_then(|file| file.project_entry_id(cx)) } } - -#[cfg(test)] -mod tests { - use crate::worktree::WorktreeHandle; - - use super::{Event, *}; - use fs::RealFs; - use futures::{future, StreamExt}; - use gpui::{executor::Deterministic, test::subscribe}; - use language::{ - tree_sitter_rust, tree_sitter_typescript, Diagnostic, FakeLspAdapter, LanguageConfig, - OffsetRangeExt, Point, ToPoint, - }; - use lsp::Url; - use serde_json::json; - use std::{cell::RefCell, os::unix, path::PathBuf, rc::Rc, task::Poll}; - use unindent::Unindent as _; - use util::{assert_set_eq, test::temp_tree}; - - #[gpui::test] - async fn test_populate_and_search(cx: &mut gpui::TestAppContext) { - let dir = temp_tree(json!({ - "root": { - "apple": "", - "banana": { - "carrot": { - "date": "", - "endive": "", - } - }, - "fennel": { - "grape": "", - } - } - })); - - let root_link_path = dir.path().join("root_link"); - unix::fs::symlink(&dir.path().join("root"), &root_link_path).unwrap(); - unix::fs::symlink( - &dir.path().join("root/fennel"), - &dir.path().join("root/finnochio"), - ) - .unwrap(); - - let project = Project::test(Arc::new(RealFs), [root_link_path.as_ref()], cx).await; - - project.read_with(cx, |project, cx| { - let tree = project.worktrees(cx).next().unwrap().read(cx); - assert_eq!(tree.file_count(), 5); - assert_eq!( - tree.inode_for_path("fennel/grape"), - tree.inode_for_path("finnochio/grape") - ); - }); - - let cancel_flag = Default::default(); - let results = project - .read_with(cx, |project, cx| { - project.match_paths("bna", false, false, 10, &cancel_flag, cx) - }) - .await; - assert_eq!( - results - .into_iter() - .map(|result| result.path) - .collect::>>(), - vec![ - PathBuf::from("banana/carrot/date").into(), - PathBuf::from("banana/carrot/endive").into(), - ] - ); - } - - #[gpui::test] - async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); - - let mut rust_language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut json_language = Language::new( - LanguageConfig { - name: "JSON".into(), - path_suffixes: vec!["json".to_string()], - ..Default::default() - }, - None, - ); - let mut fake_rust_servers = rust_language.set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - name: "the-rust-language-server", - capabilities: lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![".".to_string(), "::".to_string()]), - ..Default::default() - }), - ..Default::default() - }, - ..Default::default() - })); - let mut fake_json_servers = json_language.set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - name: "the-json-language-server", - capabilities: lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![":".to_string()]), - ..Default::default() - }), - ..Default::default() - }, - ..Default::default() - })); - - let fs = FakeFs::new(cx.background()); - fs.insert_tree( - "/the-root", - json!({ - "test.rs": "const A: i32 = 1;", - "test2.rs": "", - "Cargo.toml": "a = 1", - "package.json": "{\"a\": 1}", - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await; - project.update(cx, |project, _| { - project.languages.add(Arc::new(rust_language)); - project.languages.add(Arc::new(json_language)); - }); - - // Open a buffer without an associated language server. - let toml_buffer = project - .update(cx, |project, cx| { - project.open_local_buffer("/the-root/Cargo.toml", cx) - }) - .await - .unwrap(); - - // Open a buffer with an associated language server. - let rust_buffer = project - .update(cx, |project, cx| { - project.open_local_buffer("/the-root/test.rs", cx) - }) - .await - .unwrap(); - - // A server is started up, and it is notified about Rust files. - let mut fake_rust_server = fake_rust_servers.next().await.unwrap(); - assert_eq!( - fake_rust_server - .receive_notification::() - .await - .text_document, - lsp::TextDocumentItem { - uri: lsp::Url::from_file_path("/the-root/test.rs").unwrap(), - version: 0, - text: "const A: i32 = 1;".to_string(), - language_id: Default::default() - } - ); - - // The buffer is configured based on the language server's capabilities. - rust_buffer.read_with(cx, |buffer, _| { - assert_eq!( - buffer.completion_triggers(), - &[".".to_string(), "::".to_string()] - ); - }); - toml_buffer.read_with(cx, |buffer, _| { - assert!(buffer.completion_triggers().is_empty()); - }); - - // Edit a buffer. The changes are reported to the language server. - rust_buffer.update(cx, |buffer, cx| buffer.edit([(16..16, "2")], cx)); - assert_eq!( - fake_rust_server - .receive_notification::() - .await - .text_document, - lsp::VersionedTextDocumentIdentifier::new( - lsp::Url::from_file_path("/the-root/test.rs").unwrap(), - 1 - ) - ); - - // Open a third buffer with a different associated language server. - let json_buffer = project - .update(cx, |project, cx| { - project.open_local_buffer("/the-root/package.json", cx) - }) - .await - .unwrap(); - - // A json language server is started up and is only notified about the json buffer. - let mut fake_json_server = fake_json_servers.next().await.unwrap(); - assert_eq!( - fake_json_server - .receive_notification::() - .await - .text_document, - lsp::TextDocumentItem { - uri: lsp::Url::from_file_path("/the-root/package.json").unwrap(), - version: 0, - text: "{\"a\": 1}".to_string(), - language_id: Default::default() - } - ); - - // This buffer is configured based on the second language server's - // capabilities. - json_buffer.read_with(cx, |buffer, _| { - assert_eq!(buffer.completion_triggers(), &[":".to_string()]); - }); - - // When opening another buffer whose language server is already running, - // it is also configured based on the existing language server's capabilities. - let rust_buffer2 = project - .update(cx, |project, cx| { - project.open_local_buffer("/the-root/test2.rs", cx) - }) - .await - .unwrap(); - rust_buffer2.read_with(cx, |buffer, _| { - assert_eq!( - buffer.completion_triggers(), - &[".".to_string(), "::".to_string()] - ); - }); - - // Changes are reported only to servers matching the buffer's language. - toml_buffer.update(cx, |buffer, cx| buffer.edit([(5..5, "23")], cx)); - rust_buffer2.update(cx, |buffer, cx| buffer.edit([(0..0, "let x = 1;")], cx)); - assert_eq!( - fake_rust_server - .receive_notification::() - .await - .text_document, - lsp::VersionedTextDocumentIdentifier::new( - lsp::Url::from_file_path("/the-root/test2.rs").unwrap(), - 1 - ) - ); - - // Save notifications are reported to all servers. - toml_buffer - .update(cx, |buffer, cx| buffer.save(cx)) - .await - .unwrap(); - assert_eq!( - fake_rust_server - .receive_notification::() - .await - .text_document, - lsp::TextDocumentIdentifier::new( - lsp::Url::from_file_path("/the-root/Cargo.toml").unwrap() - ) - ); - assert_eq!( - fake_json_server - .receive_notification::() - .await - .text_document, - lsp::TextDocumentIdentifier::new( - lsp::Url::from_file_path("/the-root/Cargo.toml").unwrap() - ) - ); - - // Renames are reported only to servers matching the buffer's language. - fs.rename( - Path::new("/the-root/test2.rs"), - Path::new("/the-root/test3.rs"), - Default::default(), - ) - .await - .unwrap(); - assert_eq!( - fake_rust_server - .receive_notification::() - .await - .text_document, - lsp::TextDocumentIdentifier::new( - lsp::Url::from_file_path("/the-root/test2.rs").unwrap() - ), - ); - assert_eq!( - fake_rust_server - .receive_notification::() - .await - .text_document, - lsp::TextDocumentItem { - uri: lsp::Url::from_file_path("/the-root/test3.rs").unwrap(), - version: 0, - text: rust_buffer2.read_with(cx, |buffer, _| buffer.text()), - language_id: Default::default() - }, - ); - - rust_buffer2.update(cx, |buffer, cx| { - buffer.update_diagnostics( - DiagnosticSet::from_sorted_entries( - vec![DiagnosticEntry { - diagnostic: Default::default(), - range: Anchor::MIN..Anchor::MAX, - }], - &buffer.snapshot(), - ), - cx, - ); - assert_eq!( - buffer - .snapshot() - .diagnostics_in_range::<_, usize>(0..buffer.len(), false) - .count(), - 1 - ); - }); - - // When the rename changes the extension of the file, the buffer gets closed on the old - // language server and gets opened on the new one. - fs.rename( - Path::new("/the-root/test3.rs"), - Path::new("/the-root/test3.json"), - Default::default(), - ) - .await - .unwrap(); - assert_eq!( - fake_rust_server - .receive_notification::() - .await - .text_document, - lsp::TextDocumentIdentifier::new( - lsp::Url::from_file_path("/the-root/test3.rs").unwrap(), - ), - ); - assert_eq!( - fake_json_server - .receive_notification::() - .await - .text_document, - lsp::TextDocumentItem { - uri: lsp::Url::from_file_path("/the-root/test3.json").unwrap(), - version: 0, - text: rust_buffer2.read_with(cx, |buffer, _| buffer.text()), - language_id: Default::default() - }, - ); - - // We clear the diagnostics, since the language has changed. - rust_buffer2.read_with(cx, |buffer, _| { - assert_eq!( - buffer - .snapshot() - .diagnostics_in_range::<_, usize>(0..buffer.len(), false) - .count(), - 0 - ); - }); - - // The renamed file's version resets after changing language server. - rust_buffer2.update(cx, |buffer, cx| buffer.edit([(0..0, "// ")], cx)); - assert_eq!( - fake_json_server - .receive_notification::() - .await - .text_document, - lsp::VersionedTextDocumentIdentifier::new( - lsp::Url::from_file_path("/the-root/test3.json").unwrap(), - 1 - ) - ); - - // Restart language servers - project.update(cx, |project, cx| { - project.restart_language_servers_for_buffers( - vec![rust_buffer.clone(), json_buffer.clone()], - cx, - ); - }); - - let mut rust_shutdown_requests = fake_rust_server - .handle_request::(|_, _| future::ready(Ok(()))); - let mut json_shutdown_requests = fake_json_server - .handle_request::(|_, _| future::ready(Ok(()))); - futures::join!(rust_shutdown_requests.next(), json_shutdown_requests.next()); - - let mut fake_rust_server = fake_rust_servers.next().await.unwrap(); - let mut fake_json_server = fake_json_servers.next().await.unwrap(); - - // Ensure rust document is reopened in new rust language server - assert_eq!( - fake_rust_server - .receive_notification::() - .await - .text_document, - lsp::TextDocumentItem { - uri: lsp::Url::from_file_path("/the-root/test.rs").unwrap(), - version: 1, - text: rust_buffer.read_with(cx, |buffer, _| buffer.text()), - language_id: Default::default() - } - ); - - // Ensure json documents are reopened in new json language server - assert_set_eq!( - [ - fake_json_server - .receive_notification::() - .await - .text_document, - fake_json_server - .receive_notification::() - .await - .text_document, - ], - [ - lsp::TextDocumentItem { - uri: lsp::Url::from_file_path("/the-root/package.json").unwrap(), - version: 0, - text: json_buffer.read_with(cx, |buffer, _| buffer.text()), - language_id: Default::default() - }, - lsp::TextDocumentItem { - uri: lsp::Url::from_file_path("/the-root/test3.json").unwrap(), - version: 1, - text: rust_buffer2.read_with(cx, |buffer, _| buffer.text()), - language_id: Default::default() - } - ] - ); - - // Close notifications are reported only to servers matching the buffer's language. - cx.update(|_| drop(json_buffer)); - let close_message = lsp::DidCloseTextDocumentParams { - text_document: lsp::TextDocumentIdentifier::new( - lsp::Url::from_file_path("/the-root/package.json").unwrap(), - ), - }; - assert_eq!( - fake_json_server - .receive_notification::() - .await, - close_message, - ); - } - - #[gpui::test] - async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); - - let fs = FakeFs::new(cx.background()); - fs.insert_tree( - "/dir", - json!({ - "a.rs": "let a = 1;", - "b.rs": "let b = 2;" - }), - ) - .await; - - let project = Project::test(fs, ["/dir/a.rs".as_ref(), "/dir/b.rs".as_ref()], cx).await; - - let buffer_a = project - .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) - .await - .unwrap(); - let buffer_b = project - .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx)) - .await - .unwrap(); - - project.update(cx, |project, cx| { - project - .update_diagnostics( - 0, - lsp::PublishDiagnosticsParams { - uri: Url::from_file_path("/dir/a.rs").unwrap(), - version: None, - diagnostics: vec![lsp::Diagnostic { - range: lsp::Range::new( - lsp::Position::new(0, 4), - lsp::Position::new(0, 5), - ), - severity: Some(lsp::DiagnosticSeverity::ERROR), - message: "error 1".to_string(), - ..Default::default() - }], - }, - &[], - cx, - ) - .unwrap(); - project - .update_diagnostics( - 0, - lsp::PublishDiagnosticsParams { - uri: Url::from_file_path("/dir/b.rs").unwrap(), - version: None, - diagnostics: vec![lsp::Diagnostic { - range: lsp::Range::new( - lsp::Position::new(0, 4), - lsp::Position::new(0, 5), - ), - severity: Some(lsp::DiagnosticSeverity::WARNING), - message: "error 2".to_string(), - ..Default::default() - }], - }, - &[], - cx, - ) - .unwrap(); - }); - - buffer_a.read_with(cx, |buffer, _| { - let chunks = chunks_with_diagnostics(&buffer, 0..buffer.len()); - assert_eq!( - chunks - .iter() - .map(|(s, d)| (s.as_str(), *d)) - .collect::>(), - &[ - ("let ", None), - ("a", Some(DiagnosticSeverity::ERROR)), - (" = 1;", None), - ] - ); - }); - buffer_b.read_with(cx, |buffer, _| { - let chunks = chunks_with_diagnostics(&buffer, 0..buffer.len()); - assert_eq!( - chunks - .iter() - .map(|(s, d)| (s.as_str(), *d)) - .collect::>(), - &[ - ("let ", None), - ("b", Some(DiagnosticSeverity::WARNING)), - (" = 2;", None), - ] - ); - }); - } - - #[gpui::test] - async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); - - let progress_token = "the-progress-token"; - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_servers = language.set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - disk_based_diagnostics_progress_token: Some(progress_token.into()), - disk_based_diagnostics_sources: vec!["disk".into()], - ..Default::default() - })); - - let fs = FakeFs::new(cx.background()); - fs.insert_tree( - "/dir", - json!({ - "a.rs": "fn a() { A }", - "b.rs": "const y: i32 = 1", - }), - ) - .await; - - let project = Project::test(fs, ["/dir".as_ref()], cx).await; - project.update(cx, |project, _| project.languages.add(Arc::new(language))); - let worktree_id = - project.read_with(cx, |p, cx| p.worktrees(cx).next().unwrap().read(cx).id()); - - // Cause worktree to start the fake language server - let _buffer = project - .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx)) - .await - .unwrap(); - - let mut events = subscribe(&project, cx); - - let fake_server = fake_servers.next().await.unwrap(); - fake_server.start_progress(progress_token).await; - assert_eq!( - events.next().await.unwrap(), - Event::DiskBasedDiagnosticsStarted { - language_server_id: 0, - } - ); - - fake_server.notify::( - lsp::PublishDiagnosticsParams { - uri: Url::from_file_path("/dir/a.rs").unwrap(), - version: None, - diagnostics: vec![lsp::Diagnostic { - range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)), - severity: Some(lsp::DiagnosticSeverity::ERROR), - message: "undefined variable 'A'".to_string(), - ..Default::default() - }], - }, - ); - assert_eq!( - events.next().await.unwrap(), - Event::DiagnosticsUpdated { - language_server_id: 0, - path: (worktree_id, Path::new("a.rs")).into() - } - ); - - fake_server.end_progress(progress_token); - assert_eq!( - events.next().await.unwrap(), - Event::DiskBasedDiagnosticsFinished { - language_server_id: 0 - } - ); - - let buffer = project - .update(cx, |p, cx| p.open_local_buffer("/dir/a.rs", cx)) - .await - .unwrap(); - - buffer.read_with(cx, |buffer, _| { - let snapshot = buffer.snapshot(); - let diagnostics = snapshot - .diagnostics_in_range::<_, Point>(0..buffer.len(), false) - .collect::>(); - assert_eq!( - diagnostics, - &[DiagnosticEntry { - range: Point::new(0, 9)..Point::new(0, 10), - diagnostic: Diagnostic { - severity: lsp::DiagnosticSeverity::ERROR, - message: "undefined variable 'A'".to_string(), - group_id: 0, - is_primary: true, - ..Default::default() - } - }] - ) - }); - - // Ensure publishing empty diagnostics twice only results in one update event. - fake_server.notify::( - lsp::PublishDiagnosticsParams { - uri: Url::from_file_path("/dir/a.rs").unwrap(), - version: None, - diagnostics: Default::default(), - }, - ); - assert_eq!( - events.next().await.unwrap(), - Event::DiagnosticsUpdated { - language_server_id: 0, - path: (worktree_id, Path::new("a.rs")).into() - } - ); - - fake_server.notify::( - lsp::PublishDiagnosticsParams { - uri: Url::from_file_path("/dir/a.rs").unwrap(), - version: None, - diagnostics: Default::default(), - }, - ); - cx.foreground().run_until_parked(); - assert_eq!(futures::poll!(events.next()), Poll::Pending); - } - - #[gpui::test] - async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); - - let progress_token = "the-progress-token"; - let mut language = Language::new( - LanguageConfig { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - None, - ); - let mut fake_servers = language.set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - disk_based_diagnostics_sources: vec!["disk".into()], - disk_based_diagnostics_progress_token: Some(progress_token.into()), - ..Default::default() - })); - - let fs = FakeFs::new(cx.background()); - fs.insert_tree("/dir", json!({ "a.rs": "" })).await; - - let project = Project::test(fs, ["/dir".as_ref()], cx).await; - project.update(cx, |project, _| project.languages.add(Arc::new(language))); - - let buffer = project - .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) - .await - .unwrap(); - - // Simulate diagnostics starting to update. - let fake_server = fake_servers.next().await.unwrap(); - fake_server.start_progress(progress_token).await; - - // Restart the server before the diagnostics finish updating. - project.update(cx, |project, cx| { - project.restart_language_servers_for_buffers([buffer], cx); - }); - let mut events = subscribe(&project, cx); - - // Simulate the newly started server sending more diagnostics. - let fake_server = fake_servers.next().await.unwrap(); - fake_server.start_progress(progress_token).await; - assert_eq!( - events.next().await.unwrap(), - Event::DiskBasedDiagnosticsStarted { - language_server_id: 1 - } - ); - project.read_with(cx, |project, _| { - assert_eq!( - project - .language_servers_running_disk_based_diagnostics() - .collect::>(), - [1] - ); - }); - - // All diagnostics are considered done, despite the old server's diagnostic - // task never completing. - fake_server.end_progress(progress_token); - assert_eq!( - events.next().await.unwrap(), - Event::DiskBasedDiagnosticsFinished { - language_server_id: 1 - } - ); - project.read_with(cx, |project, _| { - assert_eq!( - project - .language_servers_running_disk_based_diagnostics() - .collect::>(), - [0; 0] - ); - }); - } - - #[gpui::test] - async fn test_toggling_enable_language_server( - deterministic: Arc, - cx: &mut gpui::TestAppContext, - ) { - deterministic.forbid_parking(); - - let mut rust = Language::new( - LanguageConfig { - name: Arc::from("Rust"), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - None, - ); - let mut fake_rust_servers = rust.set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - name: "rust-lsp", - ..Default::default() - })); - let mut js = Language::new( - LanguageConfig { - name: Arc::from("JavaScript"), - path_suffixes: vec!["js".to_string()], - ..Default::default() - }, - None, - ); - let mut fake_js_servers = js.set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - name: "js-lsp", - ..Default::default() - })); - - let fs = FakeFs::new(cx.background()); - fs.insert_tree("/dir", json!({ "a.rs": "", "b.js": "" })) - .await; - - let project = Project::test(fs, ["/dir".as_ref()], cx).await; - project.update(cx, |project, _| { - project.languages.add(Arc::new(rust)); - project.languages.add(Arc::new(js)); - }); - - let _rs_buffer = project - .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) - .await - .unwrap(); - let _js_buffer = project - .update(cx, |project, cx| project.open_local_buffer("/dir/b.js", cx)) - .await - .unwrap(); - - let mut fake_rust_server_1 = fake_rust_servers.next().await.unwrap(); - assert_eq!( - fake_rust_server_1 - .receive_notification::() - .await - .text_document - .uri - .as_str(), - "file:///dir/a.rs" - ); - - let mut fake_js_server = fake_js_servers.next().await.unwrap(); - assert_eq!( - fake_js_server - .receive_notification::() - .await - .text_document - .uri - .as_str(), - "file:///dir/b.js" - ); - - // Disable Rust language server, ensuring only that server gets stopped. - cx.update(|cx| { - cx.update_global(|settings: &mut Settings, _| { - settings.language_overrides.insert( - Arc::from("Rust"), - settings::LanguageSettings { - enable_language_server: Some(false), - ..Default::default() - }, - ); - }) - }); - fake_rust_server_1 - .receive_notification::() - .await; - - // Enable Rust and disable JavaScript language servers, ensuring that the - // former gets started again and that the latter stops. - cx.update(|cx| { - cx.update_global(|settings: &mut Settings, _| { - settings.language_overrides.insert( - Arc::from("Rust"), - settings::LanguageSettings { - enable_language_server: Some(true), - ..Default::default() - }, - ); - settings.language_overrides.insert( - Arc::from("JavaScript"), - settings::LanguageSettings { - enable_language_server: Some(false), - ..Default::default() - }, - ); - }) - }); - let mut fake_rust_server_2 = fake_rust_servers.next().await.unwrap(); - assert_eq!( - fake_rust_server_2 - .receive_notification::() - .await - .text_document - .uri - .as_str(), - "file:///dir/a.rs" - ); - fake_js_server - .receive_notification::() - .await; - } - - #[gpui::test] - async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); - - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_servers = language.set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - disk_based_diagnostics_sources: vec!["disk".into()], - ..Default::default() - })); - - let text = " - fn a() { A } - fn b() { BB } - fn c() { CCC } - " - .unindent(); - - let fs = FakeFs::new(cx.background()); - fs.insert_tree("/dir", json!({ "a.rs": text })).await; - - let project = Project::test(fs, ["/dir".as_ref()], cx).await; - project.update(cx, |project, _| project.languages.add(Arc::new(language))); - - let buffer = project - .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) - .await - .unwrap(); - - let mut fake_server = fake_servers.next().await.unwrap(); - let open_notification = fake_server - .receive_notification::() - .await; - - // Edit the buffer, moving the content down - buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "\n\n")], cx)); - let change_notification_1 = fake_server - .receive_notification::() - .await; - assert!( - change_notification_1.text_document.version > open_notification.text_document.version - ); - - // Report some diagnostics for the initial version of the buffer - fake_server.notify::( - lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path("/dir/a.rs").unwrap(), - version: Some(open_notification.text_document.version), - diagnostics: vec![ - lsp::Diagnostic { - range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)), - severity: Some(DiagnosticSeverity::ERROR), - message: "undefined variable 'A'".to_string(), - source: Some("disk".to_string()), - ..Default::default() - }, - lsp::Diagnostic { - range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)), - severity: Some(DiagnosticSeverity::ERROR), - message: "undefined variable 'BB'".to_string(), - source: Some("disk".to_string()), - ..Default::default() - }, - lsp::Diagnostic { - range: lsp::Range::new(lsp::Position::new(2, 9), lsp::Position::new(2, 12)), - severity: Some(DiagnosticSeverity::ERROR), - source: Some("disk".to_string()), - message: "undefined variable 'CCC'".to_string(), - ..Default::default() - }, - ], - }, - ); - - // The diagnostics have moved down since they were created. - buffer.next_notification(cx).await; - buffer.read_with(cx, |buffer, _| { - assert_eq!( - buffer - .snapshot() - .diagnostics_in_range::<_, Point>(Point::new(3, 0)..Point::new(5, 0), false) - .collect::>(), - &[ - DiagnosticEntry { - range: Point::new(3, 9)..Point::new(3, 11), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::ERROR, - message: "undefined variable 'BB'".to_string(), - is_disk_based: true, - group_id: 1, - is_primary: true, - ..Default::default() - }, - }, - DiagnosticEntry { - range: Point::new(4, 9)..Point::new(4, 12), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::ERROR, - message: "undefined variable 'CCC'".to_string(), - is_disk_based: true, - group_id: 2, - is_primary: true, - ..Default::default() - } - } - ] - ); - assert_eq!( - chunks_with_diagnostics(buffer, 0..buffer.len()), - [ - ("\n\nfn a() { ".to_string(), None), - ("A".to_string(), Some(DiagnosticSeverity::ERROR)), - (" }\nfn b() { ".to_string(), None), - ("BB".to_string(), Some(DiagnosticSeverity::ERROR)), - (" }\nfn c() { ".to_string(), None), - ("CCC".to_string(), Some(DiagnosticSeverity::ERROR)), - (" }\n".to_string(), None), - ] - ); - assert_eq!( - chunks_with_diagnostics(buffer, Point::new(3, 10)..Point::new(4, 11)), - [ - ("B".to_string(), Some(DiagnosticSeverity::ERROR)), - (" }\nfn c() { ".to_string(), None), - ("CC".to_string(), Some(DiagnosticSeverity::ERROR)), - ] - ); - }); - - // Ensure overlapping diagnostics are highlighted correctly. - fake_server.notify::( - lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path("/dir/a.rs").unwrap(), - version: Some(open_notification.text_document.version), - diagnostics: vec![ - lsp::Diagnostic { - range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)), - severity: Some(DiagnosticSeverity::ERROR), - message: "undefined variable 'A'".to_string(), - source: Some("disk".to_string()), - ..Default::default() - }, - lsp::Diagnostic { - range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 12)), - severity: Some(DiagnosticSeverity::WARNING), - message: "unreachable statement".to_string(), - source: Some("disk".to_string()), - ..Default::default() - }, - ], - }, - ); - - buffer.next_notification(cx).await; - buffer.read_with(cx, |buffer, _| { - assert_eq!( - buffer - .snapshot() - .diagnostics_in_range::<_, Point>(Point::new(2, 0)..Point::new(3, 0), false) - .collect::>(), - &[ - DiagnosticEntry { - range: Point::new(2, 9)..Point::new(2, 12), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::WARNING, - message: "unreachable statement".to_string(), - is_disk_based: true, - group_id: 4, - is_primary: true, - ..Default::default() - } - }, - DiagnosticEntry { - range: Point::new(2, 9)..Point::new(2, 10), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::ERROR, - message: "undefined variable 'A'".to_string(), - is_disk_based: true, - group_id: 3, - is_primary: true, - ..Default::default() - }, - } - ] - ); - assert_eq!( - chunks_with_diagnostics(buffer, Point::new(2, 0)..Point::new(3, 0)), - [ - ("fn a() { ".to_string(), None), - ("A".to_string(), Some(DiagnosticSeverity::ERROR)), - (" }".to_string(), Some(DiagnosticSeverity::WARNING)), - ("\n".to_string(), None), - ] - ); - assert_eq!( - chunks_with_diagnostics(buffer, Point::new(2, 10)..Point::new(3, 0)), - [ - (" }".to_string(), Some(DiagnosticSeverity::WARNING)), - ("\n".to_string(), None), - ] - ); - }); - - // Keep editing the buffer and ensure disk-based diagnostics get translated according to the - // changes since the last save. - buffer.update(cx, |buffer, cx| { - buffer.edit([(Point::new(2, 0)..Point::new(2, 0), " ")], cx); - buffer.edit([(Point::new(2, 8)..Point::new(2, 10), "(x: usize)")], cx); - buffer.edit([(Point::new(3, 10)..Point::new(3, 10), "xxx")], cx); - }); - let change_notification_2 = fake_server - .receive_notification::() - .await; - assert!( - change_notification_2.text_document.version - > change_notification_1.text_document.version - ); - - // Handle out-of-order diagnostics - fake_server.notify::( - lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path("/dir/a.rs").unwrap(), - version: Some(change_notification_2.text_document.version), - diagnostics: vec![ - lsp::Diagnostic { - range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)), - severity: Some(DiagnosticSeverity::ERROR), - message: "undefined variable 'BB'".to_string(), - source: Some("disk".to_string()), - ..Default::default() - }, - lsp::Diagnostic { - range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)), - severity: Some(DiagnosticSeverity::WARNING), - message: "undefined variable 'A'".to_string(), - source: Some("disk".to_string()), - ..Default::default() - }, - ], - }, - ); - - buffer.next_notification(cx).await; - buffer.read_with(cx, |buffer, _| { - assert_eq!( - buffer - .snapshot() - .diagnostics_in_range::<_, Point>(0..buffer.len(), false) - .collect::>(), - &[ - DiagnosticEntry { - range: Point::new(2, 21)..Point::new(2, 22), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::WARNING, - message: "undefined variable 'A'".to_string(), - is_disk_based: true, - group_id: 6, - is_primary: true, - ..Default::default() - } - }, - DiagnosticEntry { - range: Point::new(3, 9)..Point::new(3, 14), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::ERROR, - message: "undefined variable 'BB'".to_string(), - is_disk_based: true, - group_id: 5, - is_primary: true, - ..Default::default() - }, - } - ] - ); - }); - } - - #[gpui::test] - async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); - - let text = concat!( - "let one = ;\n", // - "let two = \n", - "let three = 3;\n", - ); - - let fs = FakeFs::new(cx.background()); - fs.insert_tree("/dir", json!({ "a.rs": text })).await; - - let project = Project::test(fs, ["/dir".as_ref()], cx).await; - let buffer = project - .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) - .await - .unwrap(); - - project.update(cx, |project, cx| { - project - .update_buffer_diagnostics( - &buffer, - vec![ - DiagnosticEntry { - range: PointUtf16::new(0, 10)..PointUtf16::new(0, 10), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::ERROR, - message: "syntax error 1".to_string(), - ..Default::default() - }, - }, - DiagnosticEntry { - range: PointUtf16::new(1, 10)..PointUtf16::new(1, 10), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::ERROR, - message: "syntax error 2".to_string(), - ..Default::default() - }, - }, - ], - None, - cx, - ) - .unwrap(); - }); - - // An empty range is extended forward to include the following character. - // At the end of a line, an empty range is extended backward to include - // the preceding character. - buffer.read_with(cx, |buffer, _| { - let chunks = chunks_with_diagnostics(&buffer, 0..buffer.len()); - assert_eq!( - chunks - .iter() - .map(|(s, d)| (s.as_str(), *d)) - .collect::>(), - &[ - ("let one = ", None), - (";", Some(DiagnosticSeverity::ERROR)), - ("\nlet two =", None), - (" ", Some(DiagnosticSeverity::ERROR)), - ("\nlet three = 3;\n", None) - ] - ); - }); - } - - #[gpui::test] - async fn test_edits_from_lsp_with_past_version(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); - - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_servers = language.set_fake_lsp_adapter(Default::default()); - - let text = " - fn a() { - f1(); - } - fn b() { - f2(); - } - fn c() { - f3(); - } - " - .unindent(); - - let fs = FakeFs::new(cx.background()); - fs.insert_tree( - "/dir", - json!({ - "a.rs": text.clone(), - }), - ) - .await; - - let project = Project::test(fs, ["/dir".as_ref()], cx).await; - project.update(cx, |project, _| project.languages.add(Arc::new(language))); - let buffer = project - .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) - .await - .unwrap(); - - let mut fake_server = fake_servers.next().await.unwrap(); - let lsp_document_version = fake_server - .receive_notification::() - .await - .text_document - .version; - - // Simulate editing the buffer after the language server computes some edits. - buffer.update(cx, |buffer, cx| { - buffer.edit( - [( - Point::new(0, 0)..Point::new(0, 0), - "// above first function\n", - )], - cx, - ); - buffer.edit( - [( - Point::new(2, 0)..Point::new(2, 0), - " // inside first function\n", - )], - cx, - ); - buffer.edit( - [( - Point::new(6, 4)..Point::new(6, 4), - "// inside second function ", - )], - cx, - ); - - assert_eq!( - buffer.text(), - " - // above first function - fn a() { - // inside first function - f1(); - } - fn b() { - // inside second function f2(); - } - fn c() { - f3(); - } - " - .unindent() - ); - }); - - let edits = project - .update(cx, |project, cx| { - project.edits_from_lsp( - &buffer, - vec![ - // replace body of first function - lsp::TextEdit { - range: lsp::Range::new( - lsp::Position::new(0, 0), - lsp::Position::new(3, 0), - ), - new_text: " - fn a() { - f10(); - } - " - .unindent(), - }, - // edit inside second function - lsp::TextEdit { - range: lsp::Range::new( - lsp::Position::new(4, 6), - lsp::Position::new(4, 6), - ), - new_text: "00".into(), - }, - // edit inside third function via two distinct edits - lsp::TextEdit { - range: lsp::Range::new( - lsp::Position::new(7, 5), - lsp::Position::new(7, 5), - ), - new_text: "4000".into(), - }, - lsp::TextEdit { - range: lsp::Range::new( - lsp::Position::new(7, 5), - lsp::Position::new(7, 6), - ), - new_text: "".into(), - }, - ], - Some(lsp_document_version), - cx, - ) - }) - .await - .unwrap(); - - buffer.update(cx, |buffer, cx| { - for (range, new_text) in edits { - buffer.edit([(range, new_text)], cx); - } - assert_eq!( - buffer.text(), - " - // above first function - fn a() { - // inside first function - f10(); - } - fn b() { - // inside second function f200(); - } - fn c() { - f4000(); - } - " - .unindent() - ); - }); - } - - #[gpui::test] - async fn test_edits_from_lsp_with_edits_on_adjacent_lines(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); - - let text = " - use a::b; - use a::c; - - fn f() { - b(); - c(); - } - " - .unindent(); - - let fs = FakeFs::new(cx.background()); - fs.insert_tree( - "/dir", - json!({ - "a.rs": text.clone(), - }), - ) - .await; - - let project = Project::test(fs, ["/dir".as_ref()], cx).await; - let buffer = project - .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) - .await - .unwrap(); - - // Simulate the language server sending us a small edit in the form of a very large diff. - // Rust-analyzer does this when performing a merge-imports code action. - let edits = project - .update(cx, |project, cx| { - project.edits_from_lsp( - &buffer, - [ - // Replace the first use statement without editing the semicolon. - lsp::TextEdit { - range: lsp::Range::new( - lsp::Position::new(0, 4), - lsp::Position::new(0, 8), - ), - new_text: "a::{b, c}".into(), - }, - // Reinsert the remainder of the file between the semicolon and the final - // newline of the file. - lsp::TextEdit { - range: lsp::Range::new( - lsp::Position::new(0, 9), - lsp::Position::new(0, 9), - ), - new_text: "\n\n".into(), - }, - lsp::TextEdit { - range: lsp::Range::new( - lsp::Position::new(0, 9), - lsp::Position::new(0, 9), - ), - new_text: " - fn f() { - b(); - c(); - }" - .unindent(), - }, - // Delete everything after the first newline of the file. - lsp::TextEdit { - range: lsp::Range::new( - lsp::Position::new(1, 0), - lsp::Position::new(7, 0), - ), - new_text: "".into(), - }, - ], - None, - cx, - ) - }) - .await - .unwrap(); - - buffer.update(cx, |buffer, cx| { - let edits = edits - .into_iter() - .map(|(range, text)| { - ( - range.start.to_point(&buffer)..range.end.to_point(&buffer), - text, - ) - }) - .collect::>(); - - assert_eq!( - edits, - [ - (Point::new(0, 4)..Point::new(0, 8), "a::{b, c}".into()), - (Point::new(1, 0)..Point::new(2, 0), "".into()) - ] - ); - - for (range, new_text) in edits { - buffer.edit([(range, new_text)], cx); - } - assert_eq!( - buffer.text(), - " - use a::{b, c}; - - fn f() { - b(); - c(); - } - " - .unindent() - ); - }); - } - - #[gpui::test] - async fn test_invalid_edits_from_lsp(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); - - let text = " - use a::b; - use a::c; - - fn f() { - b(); - c(); - } - " - .unindent(); - - let fs = FakeFs::new(cx.background()); - fs.insert_tree( - "/dir", - json!({ - "a.rs": text.clone(), - }), - ) - .await; - - let project = Project::test(fs, ["/dir".as_ref()], cx).await; - let buffer = project - .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) - .await - .unwrap(); - - // Simulate the language server sending us edits in a non-ordered fashion, - // with ranges sometimes being inverted. - let edits = project - .update(cx, |project, cx| { - project.edits_from_lsp( - &buffer, - [ - lsp::TextEdit { - range: lsp::Range::new( - lsp::Position::new(0, 9), - lsp::Position::new(0, 9), - ), - new_text: "\n\n".into(), - }, - lsp::TextEdit { - range: lsp::Range::new( - lsp::Position::new(0, 8), - lsp::Position::new(0, 4), - ), - new_text: "a::{b, c}".into(), - }, - lsp::TextEdit { - range: lsp::Range::new( - lsp::Position::new(1, 0), - lsp::Position::new(7, 0), - ), - new_text: "".into(), - }, - lsp::TextEdit { - range: lsp::Range::new( - lsp::Position::new(0, 9), - lsp::Position::new(0, 9), - ), - new_text: " - fn f() { - b(); - c(); - }" - .unindent(), - }, - ], - None, - cx, - ) - }) - .await - .unwrap(); - - buffer.update(cx, |buffer, cx| { - let edits = edits - .into_iter() - .map(|(range, text)| { - ( - range.start.to_point(&buffer)..range.end.to_point(&buffer), - text, - ) - }) - .collect::>(); - - assert_eq!( - edits, - [ - (Point::new(0, 4)..Point::new(0, 8), "a::{b, c}".into()), - (Point::new(1, 0)..Point::new(2, 0), "".into()) - ] - ); - - for (range, new_text) in edits { - buffer.edit([(range, new_text)], cx); - } - assert_eq!( - buffer.text(), - " - use a::{b, c}; - - fn f() { - b(); - c(); - } - " - .unindent() - ); - }); - } - - fn chunks_with_diagnostics( - buffer: &Buffer, - range: Range, - ) -> Vec<(String, Option)> { - let mut chunks: Vec<(String, Option)> = Vec::new(); - for chunk in buffer.snapshot().chunks(range, true) { - if chunks.last().map_or(false, |prev_chunk| { - prev_chunk.1 == chunk.diagnostic_severity - }) { - chunks.last_mut().unwrap().0.push_str(chunk.text); - } else { - chunks.push((chunk.text.to_string(), chunk.diagnostic_severity)); - } - } - chunks - } - - #[gpui::test] - async fn test_search_worktree_without_files(cx: &mut gpui::TestAppContext) { - let dir = temp_tree(json!({ - "root": { - "dir1": {}, - "dir2": { - "dir3": {} - } - } - })); - - let project = Project::test(Arc::new(RealFs), [dir.path()], cx).await; - let cancel_flag = Default::default(); - let results = project - .read_with(cx, |project, cx| { - project.match_paths("dir", false, false, 10, &cancel_flag, cx) - }) - .await; - - assert!(results.is_empty()); - } - - #[gpui::test(iterations = 10)] - async fn test_definition(cx: &mut gpui::TestAppContext) { - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_servers = language.set_fake_lsp_adapter(Default::default()); - - let fs = FakeFs::new(cx.background()); - fs.insert_tree( - "/dir", - json!({ - "a.rs": "const fn a() { A }", - "b.rs": "const y: i32 = crate::a()", - }), - ) - .await; - - let project = Project::test(fs, ["/dir/b.rs".as_ref()], cx).await; - project.update(cx, |project, _| project.languages.add(Arc::new(language))); - - let buffer = project - .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx)) - .await - .unwrap(); - - let fake_server = fake_servers.next().await.unwrap(); - fake_server.handle_request::(|params, _| async move { - let params = params.text_document_position_params; - assert_eq!( - params.text_document.uri.to_file_path().unwrap(), - Path::new("/dir/b.rs"), - ); - assert_eq!(params.position, lsp::Position::new(0, 22)); - - Ok(Some(lsp::GotoDefinitionResponse::Scalar( - lsp::Location::new( - lsp::Url::from_file_path("/dir/a.rs").unwrap(), - lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)), - ), - ))) - }); - - let mut definitions = project - .update(cx, |project, cx| project.definition(&buffer, 22, cx)) - .await - .unwrap(); - - assert_eq!(definitions.len(), 1); - let definition = definitions.pop().unwrap(); - cx.update(|cx| { - let target_buffer = definition.target.buffer.read(cx); - assert_eq!( - target_buffer - .file() - .unwrap() - .as_local() - .unwrap() - .abs_path(cx), - Path::new("/dir/a.rs"), - ); - assert_eq!(definition.target.range.to_offset(target_buffer), 9..10); - assert_eq!( - list_worktrees(&project, cx), - [("/dir/b.rs".as_ref(), true), ("/dir/a.rs".as_ref(), false)] - ); - - drop(definition); - }); - cx.read(|cx| { - assert_eq!(list_worktrees(&project, cx), [("/dir/b.rs".as_ref(), true)]); - }); - - fn list_worktrees<'a>( - project: &'a ModelHandle, - cx: &'a AppContext, - ) -> Vec<(&'a Path, bool)> { - project - .read(cx) - .worktrees(cx) - .map(|worktree| { - let worktree = worktree.read(cx); - ( - worktree.as_local().unwrap().abs_path().as_ref(), - worktree.is_visible(), - ) - }) - .collect::>() - } - } - - #[gpui::test] - async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) { - let mut language = Language::new( - LanguageConfig { - name: "TypeScript".into(), - path_suffixes: vec!["ts".to_string()], - ..Default::default() - }, - Some(tree_sitter_typescript::language_typescript()), - ); - let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()); - - let fs = FakeFs::new(cx.background()); - fs.insert_tree( - "/dir", - json!({ - "a.ts": "", - }), - ) - .await; - - let project = Project::test(fs, ["/dir".as_ref()], cx).await; - project.update(cx, |project, _| project.languages.add(Arc::new(language))); - let buffer = project - .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx)) - .await - .unwrap(); - - let fake_server = fake_language_servers.next().await.unwrap(); - - let text = "let a = b.fqn"; - buffer.update(cx, |buffer, cx| buffer.set_text(text, cx)); - let completions = project.update(cx, |project, cx| { - project.completions(&buffer, text.len(), cx) - }); - - fake_server - .handle_request::(|_, _| async move { - Ok(Some(lsp::CompletionResponse::Array(vec![ - lsp::CompletionItem { - label: "fullyQualifiedName?".into(), - insert_text: Some("fullyQualifiedName".into()), - ..Default::default() - }, - ]))) - }) - .next() - .await; - let completions = completions.await.unwrap(); - let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); - assert_eq!(completions.len(), 1); - assert_eq!(completions[0].new_text, "fullyQualifiedName"); - assert_eq!( - completions[0].old_range.to_offset(&snapshot), - text.len() - 3..text.len() - ); - - let text = "let a = \"atoms/cmp\""; - buffer.update(cx, |buffer, cx| buffer.set_text(text, cx)); - let completions = project.update(cx, |project, cx| { - project.completions(&buffer, text.len() - 1, cx) - }); - - fake_server - .handle_request::(|_, _| async move { - Ok(Some(lsp::CompletionResponse::Array(vec![ - lsp::CompletionItem { - label: "component".into(), - ..Default::default() - }, - ]))) - }) - .next() - .await; - let completions = completions.await.unwrap(); - let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); - assert_eq!(completions.len(), 1); - assert_eq!(completions[0].new_text, "component"); - assert_eq!( - completions[0].old_range.to_offset(&snapshot), - text.len() - 4..text.len() - 1 - ); - } - - #[gpui::test(iterations = 10)] - async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) { - let mut language = Language::new( - LanguageConfig { - name: "TypeScript".into(), - path_suffixes: vec!["ts".to_string()], - ..Default::default() - }, - None, - ); - let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()); - - let fs = FakeFs::new(cx.background()); - fs.insert_tree( - "/dir", - json!({ - "a.ts": "a", - }), - ) - .await; - - let project = Project::test(fs, ["/dir".as_ref()], cx).await; - project.update(cx, |project, _| project.languages.add(Arc::new(language))); - let buffer = project - .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx)) - .await - .unwrap(); - - let fake_server = fake_language_servers.next().await.unwrap(); - - // Language server returns code actions that contain commands, and not edits. - let actions = project.update(cx, |project, cx| project.code_actions(&buffer, 0..0, cx)); - fake_server - .handle_request::(|_, _| async move { - Ok(Some(vec![ - lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction { - title: "The code action".into(), - command: Some(lsp::Command { - title: "The command".into(), - command: "_the/command".into(), - arguments: Some(vec![json!("the-argument")]), - }), - ..Default::default() - }), - lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction { - title: "two".into(), - ..Default::default() - }), - ])) - }) - .next() - .await; - - let action = actions.await.unwrap()[0].clone(); - let apply = project.update(cx, |project, cx| { - project.apply_code_action(buffer.clone(), action, true, cx) - }); - - // Resolving the code action does not populate its edits. In absence of - // edits, we must execute the given command. - fake_server.handle_request::( - |action, _| async move { Ok(action) }, - ); - - // While executing the command, the language server sends the editor - // a `workspaceEdit` request. - fake_server - .handle_request::({ - let fake = fake_server.clone(); - move |params, _| { - assert_eq!(params.command, "_the/command"); - let fake = fake.clone(); - async move { - fake.server - .request::( - lsp::ApplyWorkspaceEditParams { - label: None, - edit: lsp::WorkspaceEdit { - changes: Some( - [( - lsp::Url::from_file_path("/dir/a.ts").unwrap(), - vec![lsp::TextEdit { - range: lsp::Range::new( - lsp::Position::new(0, 0), - lsp::Position::new(0, 0), - ), - new_text: "X".into(), - }], - )] - .into_iter() - .collect(), - ), - ..Default::default() - }, - }, - ) - .await - .unwrap(); - Ok(Some(json!(null))) - } - } - }) - .next() - .await; - - // Applying the code action returns a project transaction containing the edits - // sent by the language server in its `workspaceEdit` request. - let transaction = apply.await.unwrap(); - assert!(transaction.0.contains_key(&buffer)); - buffer.update(cx, |buffer, cx| { - assert_eq!(buffer.text(), "Xa"); - buffer.undo(cx); - assert_eq!(buffer.text(), "a"); - }); - } - - #[gpui::test] - async fn test_save_file(cx: &mut gpui::TestAppContext) { - let fs = FakeFs::new(cx.background()); - fs.insert_tree( - "/dir", - json!({ - "file1": "the old contents", - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; - let buffer = project - .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx)) - .await - .unwrap(); - buffer - .update(cx, |buffer, cx| { - assert_eq!(buffer.text(), "the old contents"); - buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], cx); - buffer.save(cx) - }) - .await - .unwrap(); - - let new_text = fs.load(Path::new("/dir/file1")).await.unwrap(); - assert_eq!(new_text, buffer.read_with(cx, |buffer, _| buffer.text())); - } - - #[gpui::test] - async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) { - let fs = FakeFs::new(cx.background()); - fs.insert_tree( - "/dir", - json!({ - "file1": "the old contents", - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/dir/file1".as_ref()], cx).await; - let buffer = project - .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx)) - .await - .unwrap(); - buffer - .update(cx, |buffer, cx| { - buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], cx); - buffer.save(cx) - }) - .await - .unwrap(); - - let new_text = fs.load(Path::new("/dir/file1")).await.unwrap(); - assert_eq!(new_text, buffer.read_with(cx, |buffer, _| buffer.text())); - } - - #[gpui::test] - async fn test_save_as(cx: &mut gpui::TestAppContext) { - let fs = FakeFs::new(cx.background()); - fs.insert_tree("/dir", json!({})).await; - - let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; - let buffer = project.update(cx, |project, cx| { - project.create_buffer("", None, cx).unwrap() - }); - buffer.update(cx, |buffer, cx| { - buffer.edit([(0..0, "abc")], cx); - assert!(buffer.is_dirty()); - assert!(!buffer.has_conflict()); - }); - project - .update(cx, |project, cx| { - project.save_buffer_as(buffer.clone(), "/dir/file1".into(), cx) - }) - .await - .unwrap(); - assert_eq!(fs.load(Path::new("/dir/file1")).await.unwrap(), "abc"); - buffer.read_with(cx, |buffer, cx| { - assert_eq!(buffer.file().unwrap().full_path(cx), Path::new("dir/file1")); - assert!(!buffer.is_dirty()); - assert!(!buffer.has_conflict()); - }); - - let opened_buffer = project - .update(cx, |project, cx| { - project.open_local_buffer("/dir/file1", cx) - }) - .await - .unwrap(); - assert_eq!(opened_buffer, buffer); - } - - #[gpui::test(retries = 5)] - async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) { - let dir = temp_tree(json!({ - "a": { - "file1": "", - "file2": "", - "file3": "", - }, - "b": { - "c": { - "file4": "", - "file5": "", - } - } - })); - - let project = Project::test(Arc::new(RealFs), [dir.path()], cx).await; - let rpc = project.read_with(cx, |p, _| p.client.clone()); - - let buffer_for_path = |path: &'static str, cx: &mut gpui::TestAppContext| { - let buffer = project.update(cx, |p, cx| p.open_local_buffer(dir.path().join(path), cx)); - async move { buffer.await.unwrap() } - }; - let id_for_path = |path: &'static str, cx: &gpui::TestAppContext| { - project.read_with(cx, |project, cx| { - let tree = project.worktrees(cx).next().unwrap(); - tree.read(cx) - .entry_for_path(path) - .expect(&format!("no entry for path {}", path)) - .id - }) - }; - - let buffer2 = buffer_for_path("a/file2", cx).await; - let buffer3 = buffer_for_path("a/file3", cx).await; - let buffer4 = buffer_for_path("b/c/file4", cx).await; - let buffer5 = buffer_for_path("b/c/file5", cx).await; - - let file2_id = id_for_path("a/file2", &cx); - let file3_id = id_for_path("a/file3", &cx); - let file4_id = id_for_path("b/c/file4", &cx); - - // Create a remote copy of this worktree. - let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap()); - let initial_snapshot = tree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()); - let (remote, load_task) = cx.update(|cx| { - Worktree::remote( - 1, - 1, - initial_snapshot.to_proto(&Default::default(), true), - rpc.clone(), - cx, - ) - }); - // tree - load_task.await; - - cx.read(|cx| { - assert!(!buffer2.read(cx).is_dirty()); - assert!(!buffer3.read(cx).is_dirty()); - assert!(!buffer4.read(cx).is_dirty()); - assert!(!buffer5.read(cx).is_dirty()); - }); - - // Rename and delete files and directories. - tree.flush_fs_events(&cx).await; - std::fs::rename(dir.path().join("a/file3"), dir.path().join("b/c/file3")).unwrap(); - std::fs::remove_file(dir.path().join("b/c/file5")).unwrap(); - std::fs::rename(dir.path().join("b/c"), dir.path().join("d")).unwrap(); - std::fs::rename(dir.path().join("a/file2"), dir.path().join("a/file2.new")).unwrap(); - tree.flush_fs_events(&cx).await; - - let expected_paths = vec![ - "a", - "a/file1", - "a/file2.new", - "b", - "d", - "d/file3", - "d/file4", - ]; - - cx.read(|app| { - assert_eq!( - tree.read(app) - .paths() - .map(|p| p.to_str().unwrap()) - .collect::>(), - expected_paths - ); - - assert_eq!(id_for_path("a/file2.new", &cx), file2_id); - assert_eq!(id_for_path("d/file3", &cx), file3_id); - assert_eq!(id_for_path("d/file4", &cx), file4_id); - - assert_eq!( - buffer2.read(app).file().unwrap().path().as_ref(), - Path::new("a/file2.new") - ); - assert_eq!( - buffer3.read(app).file().unwrap().path().as_ref(), - Path::new("d/file3") - ); - assert_eq!( - buffer4.read(app).file().unwrap().path().as_ref(), - Path::new("d/file4") - ); - assert_eq!( - buffer5.read(app).file().unwrap().path().as_ref(), - Path::new("b/c/file5") - ); - - assert!(!buffer2.read(app).file().unwrap().is_deleted()); - assert!(!buffer3.read(app).file().unwrap().is_deleted()); - assert!(!buffer4.read(app).file().unwrap().is_deleted()); - assert!(buffer5.read(app).file().unwrap().is_deleted()); - }); - - // Update the remote worktree. Check that it becomes consistent with the - // local worktree. - remote.update(cx, |remote, cx| { - let update_message = tree.read(cx).as_local().unwrap().snapshot().build_update( - &initial_snapshot, - 1, - 1, - true, - ); - remote - .as_remote_mut() - .unwrap() - .snapshot - .apply_remote_update(update_message) - .unwrap(); - - assert_eq!( - remote - .paths() - .map(|p| p.to_str().unwrap()) - .collect::>(), - expected_paths - ); - }); - } - - #[gpui::test] - async fn test_buffer_deduping(cx: &mut gpui::TestAppContext) { - let fs = FakeFs::new(cx.background()); - fs.insert_tree( - "/dir", - json!({ - "a.txt": "a-contents", - "b.txt": "b-contents", - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; - - // Spawn multiple tasks to open paths, repeating some paths. - let (buffer_a_1, buffer_b, buffer_a_2) = project.update(cx, |p, cx| { - ( - p.open_local_buffer("/dir/a.txt", cx), - p.open_local_buffer("/dir/b.txt", cx), - p.open_local_buffer("/dir/a.txt", cx), - ) - }); - - let buffer_a_1 = buffer_a_1.await.unwrap(); - let buffer_a_2 = buffer_a_2.await.unwrap(); - let buffer_b = buffer_b.await.unwrap(); - assert_eq!(buffer_a_1.read_with(cx, |b, _| b.text()), "a-contents"); - assert_eq!(buffer_b.read_with(cx, |b, _| b.text()), "b-contents"); - - // There is only one buffer per path. - let buffer_a_id = buffer_a_1.id(); - assert_eq!(buffer_a_2.id(), buffer_a_id); - - // Open the same path again while it is still open. - drop(buffer_a_1); - let buffer_a_3 = project - .update(cx, |p, cx| p.open_local_buffer("/dir/a.txt", cx)) - .await - .unwrap(); - - // There's still only one buffer per path. - assert_eq!(buffer_a_3.id(), buffer_a_id); - } - - #[gpui::test] - async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) { - let fs = FakeFs::new(cx.background()); - fs.insert_tree( - "/dir", - json!({ - "file1": "abc", - "file2": "def", - "file3": "ghi", - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; - - let buffer1 = project - .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx)) - .await - .unwrap(); - let events = Rc::new(RefCell::new(Vec::new())); - - // initially, the buffer isn't dirty. - buffer1.update(cx, |buffer, cx| { - cx.subscribe(&buffer1, { - let events = events.clone(); - move |_, _, event, _| match event { - BufferEvent::Operation(_) => {} - _ => events.borrow_mut().push(event.clone()), - } - }) - .detach(); - - assert!(!buffer.is_dirty()); - assert!(events.borrow().is_empty()); - - buffer.edit([(1..2, "")], cx); - }); - - // after the first edit, the buffer is dirty, and emits a dirtied event. - buffer1.update(cx, |buffer, cx| { - assert!(buffer.text() == "ac"); - assert!(buffer.is_dirty()); - assert_eq!( - *events.borrow(), - &[language::Event::Edited, language::Event::DirtyChanged] - ); - events.borrow_mut().clear(); - buffer.did_save( - buffer.version(), - buffer.as_rope().fingerprint(), - buffer.file().unwrap().mtime(), - None, - cx, - ); - }); - - // after saving, the buffer is not dirty, and emits a saved event. - buffer1.update(cx, |buffer, cx| { - assert!(!buffer.is_dirty()); - assert_eq!(*events.borrow(), &[language::Event::Saved]); - events.borrow_mut().clear(); - - buffer.edit([(1..1, "B")], cx); - buffer.edit([(2..2, "D")], cx); - }); - - // after editing again, the buffer is dirty, and emits another dirty event. - buffer1.update(cx, |buffer, cx| { - assert!(buffer.text() == "aBDc"); - assert!(buffer.is_dirty()); - assert_eq!( - *events.borrow(), - &[ - language::Event::Edited, - language::Event::DirtyChanged, - language::Event::Edited, - ], - ); - events.borrow_mut().clear(); - - // After restoring the buffer to its previously-saved state, - // the buffer is not considered dirty anymore. - buffer.edit([(1..3, "")], cx); - assert!(buffer.text() == "ac"); - assert!(!buffer.is_dirty()); - }); - - assert_eq!( - *events.borrow(), - &[language::Event::Edited, language::Event::DirtyChanged] - ); - - // When a file is deleted, the buffer is considered dirty. - let events = Rc::new(RefCell::new(Vec::new())); - let buffer2 = project - .update(cx, |p, cx| p.open_local_buffer("/dir/file2", cx)) - .await - .unwrap(); - buffer2.update(cx, |_, cx| { - cx.subscribe(&buffer2, { - let events = events.clone(); - move |_, _, event, _| events.borrow_mut().push(event.clone()) - }) - .detach(); - }); - - fs.remove_file("/dir/file2".as_ref(), Default::default()) - .await - .unwrap(); - buffer2.condition(&cx, |b, _| b.is_dirty()).await; - assert_eq!( - *events.borrow(), - &[ - language::Event::DirtyChanged, - language::Event::FileHandleChanged - ] - ); - - // When a file is already dirty when deleted, we don't emit a Dirtied event. - let events = Rc::new(RefCell::new(Vec::new())); - let buffer3 = project - .update(cx, |p, cx| p.open_local_buffer("/dir/file3", cx)) - .await - .unwrap(); - buffer3.update(cx, |_, cx| { - cx.subscribe(&buffer3, { - let events = events.clone(); - move |_, _, event, _| events.borrow_mut().push(event.clone()) - }) - .detach(); - }); - - buffer3.update(cx, |buffer, cx| { - buffer.edit([(0..0, "x")], cx); - }); - events.borrow_mut().clear(); - fs.remove_file("/dir/file3".as_ref(), Default::default()) - .await - .unwrap(); - buffer3 - .condition(&cx, |_, _| !events.borrow().is_empty()) - .await; - assert_eq!(*events.borrow(), &[language::Event::FileHandleChanged]); - cx.read(|cx| assert!(buffer3.read(cx).is_dirty())); - } - - #[gpui::test] - async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) { - let initial_contents = "aaa\nbbbbb\nc\n"; - let fs = FakeFs::new(cx.background()); - fs.insert_tree( - "/dir", - json!({ - "the-file": initial_contents, - }), - ) - .await; - let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; - let buffer = project - .update(cx, |p, cx| p.open_local_buffer("/dir/the-file", cx)) - .await - .unwrap(); - - let anchors = (0..3) - .map(|row| buffer.read_with(cx, |b, _| b.anchor_before(Point::new(row, 1)))) - .collect::>(); - - // Change the file on disk, adding two new lines of text, and removing - // one line. - buffer.read_with(cx, |buffer, _| { - assert!(!buffer.is_dirty()); - assert!(!buffer.has_conflict()); - }); - let new_contents = "AAAA\naaa\nBB\nbbbbb\n"; - fs.save("/dir/the-file".as_ref(), &new_contents.into()) - .await - .unwrap(); - - // Because the buffer was not modified, it is reloaded from disk. Its - // contents are edited according to the diff between the old and new - // file contents. - buffer - .condition(&cx, |buffer, _| buffer.text() == new_contents) - .await; - - buffer.update(cx, |buffer, _| { - assert_eq!(buffer.text(), new_contents); - assert!(!buffer.is_dirty()); - assert!(!buffer.has_conflict()); - - let anchor_positions = anchors - .iter() - .map(|anchor| anchor.to_point(&*buffer)) - .collect::>(); - assert_eq!( - anchor_positions, - [Point::new(1, 1), Point::new(3, 1), Point::new(4, 0)] - ); - }); - - // Modify the buffer - buffer.update(cx, |buffer, cx| { - buffer.edit([(0..0, " ")], cx); - assert!(buffer.is_dirty()); - assert!(!buffer.has_conflict()); - }); - - // Change the file on disk again, adding blank lines to the beginning. - fs.save( - "/dir/the-file".as_ref(), - &"\n\n\nAAAA\naaa\nBB\nbbbbb\n".into(), - ) - .await - .unwrap(); - - // Because the buffer is modified, it doesn't reload from disk, but is - // marked as having a conflict. - buffer - .condition(&cx, |buffer, _| buffer.has_conflict()) - .await; - } - - #[gpui::test] - async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); - - let fs = FakeFs::new(cx.background()); - fs.insert_tree( - "/the-dir", - json!({ - "a.rs": " - fn foo(mut v: Vec) { - for x in &v { - v.push(1); - } - } - " - .unindent(), - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/the-dir".as_ref()], cx).await; - let buffer = project - .update(cx, |p, cx| p.open_local_buffer("/the-dir/a.rs", cx)) - .await - .unwrap(); - - let buffer_uri = Url::from_file_path("/the-dir/a.rs").unwrap(); - let message = lsp::PublishDiagnosticsParams { - uri: buffer_uri.clone(), - diagnostics: vec![ - lsp::Diagnostic { - range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)), - severity: Some(DiagnosticSeverity::WARNING), - message: "error 1".to_string(), - related_information: Some(vec![lsp::DiagnosticRelatedInformation { - location: lsp::Location { - uri: buffer_uri.clone(), - range: lsp::Range::new( - lsp::Position::new(1, 8), - lsp::Position::new(1, 9), - ), - }, - message: "error 1 hint 1".to_string(), - }]), - ..Default::default() - }, - lsp::Diagnostic { - range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)), - severity: Some(DiagnosticSeverity::HINT), - message: "error 1 hint 1".to_string(), - related_information: Some(vec![lsp::DiagnosticRelatedInformation { - location: lsp::Location { - uri: buffer_uri.clone(), - range: lsp::Range::new( - lsp::Position::new(1, 8), - lsp::Position::new(1, 9), - ), - }, - message: "original diagnostic".to_string(), - }]), - ..Default::default() - }, - lsp::Diagnostic { - range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)), - severity: Some(DiagnosticSeverity::ERROR), - message: "error 2".to_string(), - related_information: Some(vec![ - lsp::DiagnosticRelatedInformation { - location: lsp::Location { - uri: buffer_uri.clone(), - range: lsp::Range::new( - lsp::Position::new(1, 13), - lsp::Position::new(1, 15), - ), - }, - message: "error 2 hint 1".to_string(), - }, - lsp::DiagnosticRelatedInformation { - location: lsp::Location { - uri: buffer_uri.clone(), - range: lsp::Range::new( - lsp::Position::new(1, 13), - lsp::Position::new(1, 15), - ), - }, - message: "error 2 hint 2".to_string(), - }, - ]), - ..Default::default() - }, - lsp::Diagnostic { - range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)), - severity: Some(DiagnosticSeverity::HINT), - message: "error 2 hint 1".to_string(), - related_information: Some(vec![lsp::DiagnosticRelatedInformation { - location: lsp::Location { - uri: buffer_uri.clone(), - range: lsp::Range::new( - lsp::Position::new(2, 8), - lsp::Position::new(2, 17), - ), - }, - message: "original diagnostic".to_string(), - }]), - ..Default::default() - }, - lsp::Diagnostic { - range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)), - severity: Some(DiagnosticSeverity::HINT), - message: "error 2 hint 2".to_string(), - related_information: Some(vec![lsp::DiagnosticRelatedInformation { - location: lsp::Location { - uri: buffer_uri.clone(), - range: lsp::Range::new( - lsp::Position::new(2, 8), - lsp::Position::new(2, 17), - ), - }, - message: "original diagnostic".to_string(), - }]), - ..Default::default() - }, - ], - version: None, - }; - - project - .update(cx, |p, cx| p.update_diagnostics(0, message, &[], cx)) - .unwrap(); - let buffer = buffer.read_with(cx, |buffer, _| buffer.snapshot()); - - assert_eq!( - buffer - .diagnostics_in_range::<_, Point>(0..buffer.len(), false) - .collect::>(), - &[ - DiagnosticEntry { - range: Point::new(1, 8)..Point::new(1, 9), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::WARNING, - message: "error 1".to_string(), - group_id: 0, - is_primary: true, - ..Default::default() - } - }, - DiagnosticEntry { - range: Point::new(1, 8)..Point::new(1, 9), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::HINT, - message: "error 1 hint 1".to_string(), - group_id: 0, - is_primary: false, - ..Default::default() - } - }, - DiagnosticEntry { - range: Point::new(1, 13)..Point::new(1, 15), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::HINT, - message: "error 2 hint 1".to_string(), - group_id: 1, - is_primary: false, - ..Default::default() - } - }, - DiagnosticEntry { - range: Point::new(1, 13)..Point::new(1, 15), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::HINT, - message: "error 2 hint 2".to_string(), - group_id: 1, - is_primary: false, - ..Default::default() - } - }, - DiagnosticEntry { - range: Point::new(2, 8)..Point::new(2, 17), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::ERROR, - message: "error 2".to_string(), - group_id: 1, - is_primary: true, - ..Default::default() - } - } - ] - ); - - assert_eq!( - buffer.diagnostic_group::(0).collect::>(), - &[ - DiagnosticEntry { - range: Point::new(1, 8)..Point::new(1, 9), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::WARNING, - message: "error 1".to_string(), - group_id: 0, - is_primary: true, - ..Default::default() - } - }, - DiagnosticEntry { - range: Point::new(1, 8)..Point::new(1, 9), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::HINT, - message: "error 1 hint 1".to_string(), - group_id: 0, - is_primary: false, - ..Default::default() - } - }, - ] - ); - assert_eq!( - buffer.diagnostic_group::(1).collect::>(), - &[ - DiagnosticEntry { - range: Point::new(1, 13)..Point::new(1, 15), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::HINT, - message: "error 2 hint 1".to_string(), - group_id: 1, - is_primary: false, - ..Default::default() - } - }, - DiagnosticEntry { - range: Point::new(1, 13)..Point::new(1, 15), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::HINT, - message: "error 2 hint 2".to_string(), - group_id: 1, - is_primary: false, - ..Default::default() - } - }, - DiagnosticEntry { - range: Point::new(2, 8)..Point::new(2, 17), - diagnostic: Diagnostic { - severity: DiagnosticSeverity::ERROR, - message: "error 2".to_string(), - group_id: 1, - is_primary: true, - ..Default::default() - } - } - ] - ); - } - - #[gpui::test] - async fn test_rename(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); - - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_servers = language.set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions { - prepare_provider: Some(true), - work_done_progress_options: Default::default(), - })), - ..Default::default() - }, - ..Default::default() - })); - - let fs = FakeFs::new(cx.background()); - fs.insert_tree( - "/dir", - json!({ - "one.rs": "const ONE: usize = 1;", - "two.rs": "const TWO: usize = one::ONE + one::ONE;" - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; - project.update(cx, |project, _| project.languages.add(Arc::new(language))); - let buffer = project - .update(cx, |project, cx| { - project.open_local_buffer("/dir/one.rs", cx) - }) - .await - .unwrap(); - - let fake_server = fake_servers.next().await.unwrap(); - - let response = project.update(cx, |project, cx| { - project.prepare_rename(buffer.clone(), 7, cx) - }); - fake_server - .handle_request::(|params, _| async move { - assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs"); - assert_eq!(params.position, lsp::Position::new(0, 7)); - Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( - lsp::Position::new(0, 6), - lsp::Position::new(0, 9), - )))) - }) - .next() - .await - .unwrap(); - let range = response.await.unwrap().unwrap(); - let range = buffer.read_with(cx, |buffer, _| range.to_offset(buffer)); - assert_eq!(range, 6..9); - - let response = project.update(cx, |project, cx| { - project.perform_rename(buffer.clone(), 7, "THREE".to_string(), true, cx) - }); - fake_server - .handle_request::(|params, _| async move { - assert_eq!( - params.text_document_position.text_document.uri.as_str(), - "file:///dir/one.rs" - ); - assert_eq!( - params.text_document_position.position, - lsp::Position::new(0, 7) - ); - assert_eq!(params.new_name, "THREE"); - Ok(Some(lsp::WorkspaceEdit { - changes: Some( - [ - ( - lsp::Url::from_file_path("/dir/one.rs").unwrap(), - vec![lsp::TextEdit::new( - lsp::Range::new( - lsp::Position::new(0, 6), - lsp::Position::new(0, 9), - ), - "THREE".to_string(), - )], - ), - ( - lsp::Url::from_file_path("/dir/two.rs").unwrap(), - vec![ - lsp::TextEdit::new( - lsp::Range::new( - lsp::Position::new(0, 24), - lsp::Position::new(0, 27), - ), - "THREE".to_string(), - ), - lsp::TextEdit::new( - lsp::Range::new( - lsp::Position::new(0, 35), - lsp::Position::new(0, 38), - ), - "THREE".to_string(), - ), - ], - ), - ] - .into_iter() - .collect(), - ), - ..Default::default() - })) - }) - .next() - .await - .unwrap(); - let mut transaction = response.await.unwrap().0; - assert_eq!(transaction.len(), 2); - assert_eq!( - transaction - .remove_entry(&buffer) - .unwrap() - .0 - .read_with(cx, |buffer, _| buffer.text()), - "const THREE: usize = 1;" - ); - assert_eq!( - transaction - .into_keys() - .next() - .unwrap() - .read_with(cx, |buffer, _| buffer.text()), - "const TWO: usize = one::THREE + one::THREE;" - ); - } - - #[gpui::test] - async fn test_search(cx: &mut gpui::TestAppContext) { - let fs = FakeFs::new(cx.background()); - fs.insert_tree( - "/dir", - json!({ - "one.rs": "const ONE: usize = 1;", - "two.rs": "const TWO: usize = one::ONE + one::ONE;", - "three.rs": "const THREE: usize = one::ONE + two::TWO;", - "four.rs": "const FOUR: usize = one::ONE + three::THREE;", - }), - ) - .await; - let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; - assert_eq!( - search(&project, SearchQuery::text("TWO", false, true), cx) - .await - .unwrap(), - HashMap::from_iter([ - ("two.rs".to_string(), vec![6..9]), - ("three.rs".to_string(), vec![37..40]) - ]) - ); - - let buffer_4 = project - .update(cx, |project, cx| { - project.open_local_buffer("/dir/four.rs", cx) - }) - .await - .unwrap(); - buffer_4.update(cx, |buffer, cx| { - let text = "two::TWO"; - buffer.edit([(20..28, text), (31..43, text)], cx); - }); - - assert_eq!( - search(&project, SearchQuery::text("TWO", false, true), cx) - .await - .unwrap(), - HashMap::from_iter([ - ("two.rs".to_string(), vec![6..9]), - ("three.rs".to_string(), vec![37..40]), - ("four.rs".to_string(), vec![25..28, 36..39]) - ]) - ); - - async fn search( - project: &ModelHandle, - query: SearchQuery, - cx: &mut gpui::TestAppContext, - ) -> Result>>> { - let results = project - .update(cx, |project, cx| project.search(query, cx)) - .await?; - - Ok(results - .into_iter() - .map(|(buffer, ranges)| { - buffer.read_with(cx, |buffer, _| { - let path = buffer.file().unwrap().path().to_string_lossy().to_string(); - let ranges = ranges - .into_iter() - .map(|range| range.to_offset(buffer)) - .collect::>(); - (path, ranges) - }) - }) - .collect()) - } - } -} diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 00f7bb8c94..e16edf27d2 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -86,7 +86,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { }, None, ); - let mut fake_rust_servers = rust_language.set_fake_lsp_adapter(FakeLspAdapter { + let mut fake_rust_servers = rust_language.set_fake_lsp_adapter(Arc::new(FakeLspAdapter { name: "the-rust-language-server", capabilities: lsp::ServerCapabilities { completion_provider: Some(lsp::CompletionOptions { @@ -96,8 +96,8 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { ..Default::default() }, ..Default::default() - }); - let mut fake_json_servers = json_language.set_fake_lsp_adapter(FakeLspAdapter { + })); + let mut fake_json_servers = json_language.set_fake_lsp_adapter(Arc::new(FakeLspAdapter { name: "the-json-language-server", capabilities: lsp::ServerCapabilities { completion_provider: Some(lsp::CompletionOptions { @@ -107,7 +107,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { ..Default::default() }, ..Default::default() - }); + })); let fs = FakeFs::new(cx.background()); fs.insert_tree( @@ -611,11 +611,11 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) { }, Some(tree_sitter_rust::language()), ); - let mut fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter { - disk_based_diagnostics_progress_token: Some(progress_token), - disk_based_diagnostics_sources: &["disk"], + let mut fake_servers = language.set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + disk_based_diagnostics_progress_token: Some(progress_token.into()), + disk_based_diagnostics_sources: vec!["disk".into()], ..Default::default() - }); + })); let fs = FakeFs::new(cx.background()); fs.insert_tree( @@ -734,11 +734,11 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC }, None, ); - let mut fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter { - disk_based_diagnostics_sources: &["disk"], - disk_based_diagnostics_progress_token: Some(progress_token), + let mut fake_servers = language.set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + disk_based_diagnostics_sources: vec!["disk".into()], + disk_based_diagnostics_progress_token: Some(progress_token.into()), ..Default::default() - }); + })); let fs = FakeFs::new(cx.background()); fs.insert_tree("/dir", json!({ "a.rs": "" })).await; @@ -813,10 +813,10 @@ async fn test_toggling_enable_language_server( }, None, ); - let mut fake_rust_servers = rust.set_fake_lsp_adapter(FakeLspAdapter { + let mut fake_rust_servers = rust.set_fake_lsp_adapter(Arc::new(FakeLspAdapter { name: "rust-lsp", ..Default::default() - }); + })); let mut js = Language::new( LanguageConfig { name: Arc::from("JavaScript"), @@ -825,10 +825,10 @@ async fn test_toggling_enable_language_server( }, None, ); - let mut fake_js_servers = js.set_fake_lsp_adapter(FakeLspAdapter { + let mut fake_js_servers = js.set_fake_lsp_adapter(Arc::new(FakeLspAdapter { name: "js-lsp", ..Default::default() - }); + })); let fs = FakeFs::new(cx.background()); fs.insert_tree("/dir", json!({ "a.rs": "", "b.js": "" })) @@ -934,10 +934,10 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { }, Some(tree_sitter_rust::language()), ); - let mut fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter { - disk_based_diagnostics_sources: &["disk"], + let mut fake_servers = language.set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + disk_based_diagnostics_sources: vec!["disk".into()], ..Default::default() - }); + })); let text = " fn a() { A } @@ -2841,7 +2841,7 @@ async fn test_rename(cx: &mut gpui::TestAppContext) { }, Some(tree_sitter_rust::language()), ); - let mut fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter { + let mut fake_servers = language.set_fake_lsp_adapter(Arc::new(FakeLspAdapter { capabilities: lsp::ServerCapabilities { rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions { prepare_provider: Some(true), @@ -2850,7 +2850,7 @@ async fn test_rename(cx: &mut gpui::TestAppContext) { ..Default::default() }, ..Default::default() - }); + })); let fs = FakeFs::new(cx.background()); fs.insert_tree(