project: Revert project tree impl (again) (#23572)
This commit is contained in:
parent
2a2c332584
commit
d8c9fdd014
29 changed files with 943 additions and 2219 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -7395,7 +7395,6 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"smol",
|
||||
"text",
|
||||
"util",
|
||||
]
|
||||
|
||||
|
@ -9832,7 +9831,6 @@ dependencies = [
|
|||
"log",
|
||||
"lsp",
|
||||
"node_runtime",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"pathdiff",
|
||||
"paths",
|
||||
|
|
|
@ -372,7 +372,7 @@ async-tungstenite = "0.28"
|
|||
async-watch = "0.3.1"
|
||||
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
|
||||
base64 = "0.22"
|
||||
bitflags = "2.8.0"
|
||||
bitflags = "2.6.0"
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "091a8401033847bb9b6ace3fcf70448d069621c5" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "091a8401033847bb9b6ace3fcf70448d069621c5" }
|
||||
blade-util = { git = "https://github.com/kvark/blade", rev = "091a8401033847bb9b6ace3fcf70448d069621c5" }
|
||||
|
@ -421,13 +421,12 @@ libc = "0.2"
|
|||
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
|
||||
linkify = "0.10.0"
|
||||
livekit = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev="060964da10574cd9bf06463a53bf6e0769c5c45e", features = ["dispatcher", "services-dispatcher", "rustls-tls-native-roots"], default-features = false }
|
||||
log = { version = "0.4.25", features = ["kv_unstable_serde", "serde"] }
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
|
||||
markup5ever_rcdom = "0.3.0"
|
||||
nanoid = "0.4"
|
||||
nbformat = { version = "0.10.0" }
|
||||
nix = "0.29"
|
||||
num-format = "0.4.4"
|
||||
once_cell = "1.20"
|
||||
ordered-float = "2.1.1"
|
||||
palette = { version = "0.7.5", default-features = false, features = ["std"] }
|
||||
parking_lot = "0.12.1"
|
||||
|
@ -510,7 +509,7 @@ tree-sitter = { version = "0.23", features = ["wasm"] }
|
|||
tree-sitter-bash = "0.23"
|
||||
tree-sitter-c = "0.23"
|
||||
tree-sitter-cpp = "0.23"
|
||||
tree-sitter-css = "0.23.2"
|
||||
tree-sitter-css = "0.23"
|
||||
tree-sitter-elixir = "0.3"
|
||||
tree-sitter-embedded-template = "0.23.0"
|
||||
tree-sitter-go = "0.23"
|
||||
|
|
|
@ -53,7 +53,7 @@ reqwest_client.workspace = true
|
|||
rpc.workspace = true
|
||||
rustc-demangle.workspace = true
|
||||
scrypt = "0.11"
|
||||
sea-orm = { version = "1.1.4", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls", "with-uuid"] }
|
||||
sea-orm = { version = "1.1.0-rc.1", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls", "with-uuid"] }
|
||||
semantic_version.workspace = true
|
||||
semver.workspace = true
|
||||
serde.workspace = true
|
||||
|
@ -116,7 +116,7 @@ release_channel.workspace = true
|
|||
remote = { workspace = true, features = ["test-support"] }
|
||||
remote_server.workspace = true
|
||||
rpc = { workspace = true, features = ["test-support"] }
|
||||
sea-orm = { version = "1.1.4", features = ["sqlx-sqlite"] }
|
||||
sea-orm = { version = "1.1.0-rc.1", features = ["sqlx-sqlite"] }
|
||||
serde_json.workspace = true
|
||||
session = { workspace = true, features = ["test-support"] }
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
|
|
|
@ -461,14 +461,12 @@ impl Copilot {
|
|||
.on_notification::<StatusNotification, _>(|_, _| { /* Silence the notification */ })
|
||||
.detach();
|
||||
|
||||
let initialize_params = None;
|
||||
let configuration = lsp::DidChangeConfigurationParams {
|
||||
settings: Default::default(),
|
||||
};
|
||||
let server = cx
|
||||
.update(|cx| {
|
||||
let params = server.default_initialize_params(cx);
|
||||
server.initialize(params, configuration.into(), cx)
|
||||
})?
|
||||
.update(|cx| server.initialize(initialize_params, configuration.into(), cx))?
|
||||
.await?;
|
||||
|
||||
let status = server
|
||||
|
|
|
@ -12480,27 +12480,28 @@ impl Editor {
|
|||
cx.emit(SearchEvent::MatchesInvalidated);
|
||||
if *singleton_buffer_edited {
|
||||
if let Some(project) = &self.project {
|
||||
let project = project.read(cx);
|
||||
#[allow(clippy::mutable_key_type)]
|
||||
let languages_affected = multibuffer.update(cx, |multibuffer, cx| {
|
||||
multibuffer
|
||||
.all_buffers()
|
||||
.into_iter()
|
||||
.filter_map(|buffer| {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
let language = buffer.language()?;
|
||||
let should_discard = project.update(cx, |project, cx| {
|
||||
project.is_local()
|
||||
&& project.for_language_servers_for_local_buffer(
|
||||
buffer,
|
||||
|it| it.count() == 0,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
should_discard.not().then_some(language.clone())
|
||||
})
|
||||
})
|
||||
.collect::<HashSet<_>>()
|
||||
});
|
||||
let languages_affected = multibuffer
|
||||
.read(cx)
|
||||
.all_buffers()
|
||||
.into_iter()
|
||||
.filter_map(|buffer| {
|
||||
let buffer = buffer.read(cx);
|
||||
let language = buffer.language()?;
|
||||
if project.is_local()
|
||||
&& project
|
||||
.language_servers_for_local_buffer(buffer, cx)
|
||||
.count()
|
||||
== 0
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some(language)
|
||||
}
|
||||
})
|
||||
.cloned()
|
||||
.collect::<HashSet<_>>();
|
||||
if !languages_affected.is_empty() {
|
||||
self.refresh_inlay_hints(
|
||||
InlayHintRefreshReason::BufferEdited(languages_affected),
|
||||
|
@ -13054,18 +13055,15 @@ impl Editor {
|
|||
self.handle_input(text, cx);
|
||||
}
|
||||
|
||||
pub fn supports_inlay_hints(&self, cx: &mut AppContext) -> bool {
|
||||
pub fn supports_inlay_hints(&self, cx: &AppContext) -> bool {
|
||||
let Some(provider) = self.semantics_provider.as_ref() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let mut supports = false;
|
||||
self.buffer().update(cx, |this, cx| {
|
||||
this.for_each_buffer(|buffer| {
|
||||
supports |= provider.supports_inlay_hints(buffer, cx);
|
||||
})
|
||||
self.buffer().read(cx).for_each_buffer(|buffer| {
|
||||
supports |= provider.supports_inlay_hints(buffer, cx);
|
||||
});
|
||||
|
||||
supports
|
||||
}
|
||||
|
||||
|
@ -13677,7 +13675,7 @@ pub trait SemanticsProvider {
|
|||
cx: &mut AppContext,
|
||||
) -> Option<Task<anyhow::Result<InlayHint>>>;
|
||||
|
||||
fn supports_inlay_hints(&self, buffer: &Model<Buffer>, cx: &mut AppContext) -> bool;
|
||||
fn supports_inlay_hints(&self, buffer: &Model<Buffer>, cx: &AppContext) -> bool;
|
||||
|
||||
fn document_highlights(
|
||||
&self,
|
||||
|
@ -14062,25 +14060,17 @@ impl SemanticsProvider for Model<Project> {
|
|||
}))
|
||||
}
|
||||
|
||||
fn supports_inlay_hints(&self, buffer: &Model<Buffer>, cx: &mut AppContext) -> bool {
|
||||
fn supports_inlay_hints(&self, buffer: &Model<Buffer>, cx: &AppContext) -> bool {
|
||||
// TODO: make this work for remote projects
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
self.update(cx, |this, cx| {
|
||||
this.for_language_servers_for_local_buffer(
|
||||
buffer,
|
||||
|mut it| {
|
||||
it.any(
|
||||
|(_, server)| match server.capabilities().inlay_hint_provider {
|
||||
Some(lsp::OneOf::Left(enabled)) => enabled,
|
||||
Some(lsp::OneOf::Right(_)) => true,
|
||||
None => false,
|
||||
},
|
||||
)
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
self.read(cx)
|
||||
.language_servers_for_local_buffer(buffer.read(cx), cx)
|
||||
.any(
|
||||
|(_, server)| match server.capabilities().inlay_hint_provider {
|
||||
Some(lsp::OneOf::Left(enabled)) => enabled,
|
||||
Some(lsp::OneOf::Right(_)) => true,
|
||||
None => false,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn inlay_hints(
|
||||
|
|
|
@ -10742,6 +10742,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test
|
|||
0,
|
||||
"Should not restart LSP server on an unrelated LSP settings change"
|
||||
);
|
||||
|
||||
update_test_project_settings(cx, |project_settings| {
|
||||
project_settings.lsp.insert(
|
||||
language_server_name.into(),
|
||||
|
|
|
@ -11,7 +11,7 @@ use multi_buffer::Anchor;
|
|||
|
||||
pub(crate) fn find_specific_language_server_in_selection<F>(
|
||||
editor: &Editor,
|
||||
cx: &mut WindowContext,
|
||||
cx: &WindowContext,
|
||||
filter_language: F,
|
||||
language_server_name: &str,
|
||||
) -> Option<(Anchor, Arc<Language>, LanguageServerId, Model<Buffer>)>
|
||||
|
@ -21,6 +21,7 @@ where
|
|||
let Some(project) = &editor.project else {
|
||||
return None;
|
||||
};
|
||||
let multibuffer = editor.buffer().read(cx);
|
||||
let mut language_servers_for = HashMap::default();
|
||||
editor
|
||||
.selections
|
||||
|
@ -28,33 +29,29 @@ where
|
|||
.iter()
|
||||
.filter(|selection| selection.start == selection.end)
|
||||
.filter_map(|selection| Some((selection.start.buffer_id?, selection.start)))
|
||||
.find_map(|(buffer_id, trigger_anchor)| {
|
||||
let buffer = editor.buffer().read(cx).buffer(buffer_id)?;
|
||||
.filter_map(|(buffer_id, trigger_anchor)| {
|
||||
let buffer = multibuffer.buffer(buffer_id)?;
|
||||
let server_id = *match language_servers_for.entry(buffer_id) {
|
||||
Entry::Occupied(occupied_entry) => occupied_entry.into_mut(),
|
||||
Entry::Vacant(vacant_entry) => {
|
||||
let language_server_id = buffer.update(cx, |buffer, cx| {
|
||||
project.update(cx, |project, cx| {
|
||||
project.for_language_servers_for_local_buffer(
|
||||
buffer,
|
||||
|mut it| {
|
||||
it.find_map(|(adapter, server)| {
|
||||
if adapter.name.0.as_ref() == language_server_name {
|
||||
Some(server.server_id())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
});
|
||||
let language_server_id = project
|
||||
.read(cx)
|
||||
.language_servers_for_local_buffer(buffer.read(cx), cx)
|
||||
.find_map(|(adapter, server)| {
|
||||
if adapter.name.0.as_ref() == language_server_name {
|
||||
Some(server.server_id())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
vacant_entry.insert(language_server_id)
|
||||
}
|
||||
}
|
||||
.as_ref()?;
|
||||
|
||||
Some((buffer, trigger_anchor, server_id))
|
||||
})
|
||||
.find_map(|(buffer, trigger_anchor, server_id)| {
|
||||
let language = buffer.read(cx).language_at(trigger_anchor.text_anchor)?;
|
||||
if !filter_language(&language) {
|
||||
return None;
|
||||
|
|
|
@ -455,7 +455,7 @@ impl SemanticsProvider for BranchBufferSemanticsProvider {
|
|||
self.0.resolve_inlay_hint(hint, buffer, server_id, cx)
|
||||
}
|
||||
|
||||
fn supports_inlay_hints(&self, buffer: &Model<Buffer>, cx: &mut AppContext) -> bool {
|
||||
fn supports_inlay_hints(&self, buffer: &Model<Buffer>, cx: &AppContext) -> bool {
|
||||
if let Some(buffer) = self.to_base(&buffer, &[], cx) {
|
||||
self.0.supports_inlay_hints(&buffer, cx)
|
||||
} else {
|
||||
|
|
|
@ -14,6 +14,6 @@ proc-macro = true
|
|||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "1.0.93"
|
||||
proc-macro2 = "1.0.66"
|
||||
quote = "1.0.9"
|
||||
syn = { version = "1.0.72", features = ["full", "extra-traits"] }
|
||||
|
|
|
@ -46,6 +46,7 @@ use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
|||
use serde_json::Value;
|
||||
use settings::WorktreeId;
|
||||
use smol::future::FutureExt as _;
|
||||
use std::num::NonZeroU32;
|
||||
use std::{
|
||||
any::Any,
|
||||
ffi::OsStr,
|
||||
|
@ -61,7 +62,6 @@ use std::{
|
|||
Arc, LazyLock,
|
||||
},
|
||||
};
|
||||
use std::{num::NonZeroU32, sync::OnceLock};
|
||||
use syntax_map::{QueryCursorHandle, SyntaxSnapshot};
|
||||
use task::RunnableTag;
|
||||
pub use task_context::{ContextProvider, RunnableRange};
|
||||
|
@ -164,7 +164,6 @@ pub struct CachedLspAdapter {
|
|||
pub adapter: Arc<dyn LspAdapter>,
|
||||
pub reinstall_attempt_count: AtomicU64,
|
||||
cached_binary: futures::lock::Mutex<Option<LanguageServerBinary>>,
|
||||
attach_kind: OnceLock<Attach>,
|
||||
}
|
||||
|
||||
impl Debug for CachedLspAdapter {
|
||||
|
@ -200,7 +199,6 @@ impl CachedLspAdapter {
|
|||
adapter,
|
||||
cached_binary: Default::default(),
|
||||
reinstall_attempt_count: AtomicU64::new(0),
|
||||
attach_kind: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -262,38 +260,6 @@ impl CachedLspAdapter {
|
|||
.cloned()
|
||||
.unwrap_or_else(|| language_name.lsp_id())
|
||||
}
|
||||
pub fn find_project_root(
|
||||
&self,
|
||||
path: &Path,
|
||||
ancestor_depth: usize,
|
||||
delegate: &Arc<dyn LspAdapterDelegate>,
|
||||
) -> Option<Arc<Path>> {
|
||||
self.adapter
|
||||
.find_project_root(path, ancestor_depth, delegate)
|
||||
}
|
||||
pub fn attach_kind(&self) -> Attach {
|
||||
*self.attach_kind.get_or_init(|| self.adapter.attach_kind())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Attach {
|
||||
/// Create a single language server instance per subproject root.
|
||||
InstancePerRoot,
|
||||
/// Use one shared language server instance for all subprojects within a project.
|
||||
Shared,
|
||||
}
|
||||
|
||||
impl Attach {
|
||||
pub fn root_path(
|
||||
&self,
|
||||
root_subproject_path: (WorktreeId, Arc<Path>),
|
||||
) -> (WorktreeId, Arc<Path>) {
|
||||
match self {
|
||||
Attach::InstancePerRoot => root_subproject_path,
|
||||
Attach::Shared => (root_subproject_path.0, Arc::from(Path::new(""))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// [`LspAdapterDelegate`] allows [`LspAdapter]` implementations to interface with the application
|
||||
|
@ -542,19 +508,6 @@ pub trait LspAdapter: 'static + Send + Sync {
|
|||
fn prepare_initialize_params(&self, original: InitializeParams) -> Result<InitializeParams> {
|
||||
Ok(original)
|
||||
}
|
||||
fn attach_kind(&self) -> Attach {
|
||||
Attach::Shared
|
||||
}
|
||||
fn find_project_root(
|
||||
&self,
|
||||
|
||||
_path: &Path,
|
||||
_ancestor_depth: usize,
|
||||
_: &Arc<dyn LspAdapterDelegate>,
|
||||
) -> Option<Arc<Path>> {
|
||||
// By default all language servers are rooted at the root of the worktree.
|
||||
Some(Arc::from("".as_ref()))
|
||||
}
|
||||
}
|
||||
|
||||
async fn try_fetch_server_binary<L: LspAdapter + 'static + Send + Sync + ?Sized>(
|
||||
|
|
|
@ -96,7 +96,6 @@ struct LanguageRegistryState {
|
|||
available_languages: Vec<AvailableLanguage>,
|
||||
grammars: HashMap<Arc<str>, AvailableGrammar>,
|
||||
lsp_adapters: HashMap<LanguageName, Vec<Arc<CachedLspAdapter>>>,
|
||||
all_lsp_adapters: HashMap<LanguageServerName, Arc<CachedLspAdapter>>,
|
||||
available_lsp_adapters:
|
||||
HashMap<LanguageServerName, Arc<dyn Fn() -> Arc<CachedLspAdapter> + 'static + Send + Sync>>,
|
||||
loading_languages: HashMap<LanguageId, Vec<oneshot::Sender<Result<Arc<Language>>>>>,
|
||||
|
@ -223,7 +222,6 @@ impl LanguageRegistry {
|
|||
language_settings: Default::default(),
|
||||
loading_languages: Default::default(),
|
||||
lsp_adapters: Default::default(),
|
||||
all_lsp_adapters: Default::default(),
|
||||
available_lsp_adapters: HashMap::default(),
|
||||
subscription: watch::channel(),
|
||||
theme: Default::default(),
|
||||
|
@ -346,16 +344,12 @@ impl LanguageRegistry {
|
|||
adapter: Arc<dyn LspAdapter>,
|
||||
) -> Arc<CachedLspAdapter> {
|
||||
let cached = CachedLspAdapter::new(adapter);
|
||||
let mut state = self.state.write();
|
||||
state
|
||||
self.state
|
||||
.write()
|
||||
.lsp_adapters
|
||||
.entry(language_name)
|
||||
.or_default()
|
||||
.push(cached.clone());
|
||||
state
|
||||
.all_lsp_adapters
|
||||
.insert(cached.name.clone(), cached.clone());
|
||||
|
||||
cached
|
||||
}
|
||||
|
||||
|
@ -395,17 +389,12 @@ impl LanguageRegistry {
|
|||
let adapter_name = LanguageServerName(adapter.name.into());
|
||||
let capabilities = adapter.capabilities.clone();
|
||||
let initializer = adapter.initializer.take();
|
||||
let adapter = CachedLspAdapter::new(Arc::new(adapter));
|
||||
{
|
||||
let mut state = self.state.write();
|
||||
state
|
||||
.lsp_adapters
|
||||
.entry(language_name.clone())
|
||||
.or_default()
|
||||
.push(adapter.clone());
|
||||
state.all_lsp_adapters.insert(adapter.name(), adapter);
|
||||
}
|
||||
|
||||
self.state
|
||||
.write()
|
||||
.lsp_adapters
|
||||
.entry(language_name.clone())
|
||||
.or_default()
|
||||
.push(CachedLspAdapter::new(Arc::new(adapter)));
|
||||
self.register_fake_language_server(adapter_name, capabilities, initializer)
|
||||
}
|
||||
|
||||
|
@ -418,16 +407,12 @@ impl LanguageRegistry {
|
|||
adapter: crate::FakeLspAdapter,
|
||||
) {
|
||||
let language_name = language_name.into();
|
||||
let mut state = self.state.write();
|
||||
let cached_adapter = CachedLspAdapter::new(Arc::new(adapter));
|
||||
state
|
||||
self.state
|
||||
.write()
|
||||
.lsp_adapters
|
||||
.entry(language_name.clone())
|
||||
.or_default()
|
||||
.push(cached_adapter.clone());
|
||||
state
|
||||
.all_lsp_adapters
|
||||
.insert(cached_adapter.name(), cached_adapter);
|
||||
.push(CachedLspAdapter::new(Arc::new(adapter)));
|
||||
}
|
||||
|
||||
/// Register a fake language server (without the adapter)
|
||||
|
@ -895,10 +880,6 @@ impl LanguageRegistry {
|
|||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn adapter_for_name(&self, name: &LanguageServerName) -> Option<Arc<CachedLspAdapter>> {
|
||||
self.state.read().all_lsp_adapters.get(name).cloned()
|
||||
}
|
||||
|
||||
pub fn update_lsp_status(
|
||||
&self,
|
||||
server_name: LanguageServerName,
|
||||
|
|
|
@ -730,8 +730,7 @@ impl LspLogView {
|
|||
|
||||
* Binary: {BINARY:#?}
|
||||
|
||||
* Registered workspace folders:
|
||||
{WORKSPACE_FOLDERS}
|
||||
* Running in project: {PATH:?}
|
||||
|
||||
* Capabilities: {CAPABILITIES}
|
||||
|
||||
|
@ -739,15 +738,7 @@ impl LspLogView {
|
|||
NAME = server.name(),
|
||||
ID = server.server_id(),
|
||||
BINARY = server.binary(),
|
||||
WORKSPACE_FOLDERS = server
|
||||
.workspace_folders()
|
||||
.iter()
|
||||
.filter_map(|path| path
|
||||
.to_file_path()
|
||||
.ok()
|
||||
.map(|path| path.to_string_lossy().into_owned()))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
PATH = server.root_path(),
|
||||
CAPABILITIES = serde_json::to_string_pretty(&server.capabilities())
|
||||
.unwrap_or_else(|e| format!("Failed to serialize capabilities: {e}")),
|
||||
CONFIGURATION = serde_json::to_string_pretty(server.configuration())
|
||||
|
|
|
@ -74,22 +74,6 @@ impl LspAdapter for RustLspAdapter {
|
|||
Self::SERVER_NAME.clone()
|
||||
}
|
||||
|
||||
fn find_project_root(
|
||||
&self,
|
||||
path: &Path,
|
||||
ancestor_depth: usize,
|
||||
delegate: &Arc<dyn LspAdapterDelegate>,
|
||||
) -> Option<Arc<Path>> {
|
||||
let mut outermost_cargo_toml = None;
|
||||
for path in path.ancestors().take(ancestor_depth) {
|
||||
let p = path.join("Cargo.toml").to_path_buf();
|
||||
if smol::block_on(delegate.read_text_file(p)).is_ok() {
|
||||
outermost_cargo_toml = Some(Arc::from(path));
|
||||
}
|
||||
}
|
||||
|
||||
outermost_cargo_toml
|
||||
}
|
||||
async fn check_if_user_installed(
|
||||
&self,
|
||||
delegate: &dyn LspAdapterDelegate,
|
||||
|
|
|
@ -29,7 +29,6 @@ serde.workspace = true
|
|||
serde_json.workspace = true
|
||||
schemars.workspace = true
|
||||
smol.workspace = true
|
||||
text.workspace = true
|
||||
util.workspace = true
|
||||
release_channel.workspace = true
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@ use anyhow::{anyhow, Context, Result};
|
|||
use collections::HashMap;
|
||||
use futures::{channel::oneshot, io::BufWriter, select, AsyncRead, AsyncWrite, Future, FutureExt};
|
||||
use gpui::{AppContext, AsyncAppContext, BackgroundExecutor, SharedString, Task};
|
||||
use notification::DidChangeWorkspaceFolders;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use postage::{barrier, prelude::Stream};
|
||||
use schemars::{
|
||||
|
@ -22,14 +21,12 @@ use smol::{
|
|||
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
|
||||
process::Child,
|
||||
};
|
||||
use text::BufferId;
|
||||
|
||||
use std::{
|
||||
collections::BTreeSet,
|
||||
ffi::{OsStr, OsString},
|
||||
fmt,
|
||||
io::Write,
|
||||
ops::{Deref, DerefMut},
|
||||
ops::DerefMut,
|
||||
path::PathBuf,
|
||||
pin::Pin,
|
||||
sync::{
|
||||
|
@ -99,9 +96,9 @@ pub struct LanguageServer {
|
|||
#[allow(clippy::type_complexity)]
|
||||
io_tasks: Mutex<Option<(Task<Option<()>>, Task<Option<()>>)>>,
|
||||
output_done_rx: Mutex<Option<barrier::Receiver>>,
|
||||
root_path: PathBuf,
|
||||
working_dir: PathBuf,
|
||||
server: Arc<Mutex<Option<Child>>>,
|
||||
workspace_folders: Arc<Mutex<BTreeSet<Url>>>,
|
||||
registered_buffers: Arc<Mutex<HashMap<BufferId, Url>>>,
|
||||
}
|
||||
|
||||
/// Identifies a running language server.
|
||||
|
@ -379,6 +376,8 @@ impl LanguageServer {
|
|||
Some(stderr),
|
||||
stderr_capture,
|
||||
Some(server),
|
||||
root_path,
|
||||
working_dir,
|
||||
code_action_kinds,
|
||||
binary,
|
||||
cx,
|
||||
|
@ -404,6 +403,8 @@ impl LanguageServer {
|
|||
stderr: Option<Stderr>,
|
||||
stderr_capture: Arc<Mutex<Option<String>>>,
|
||||
server: Option<Child>,
|
||||
root_path: &Path,
|
||||
working_dir: &Path,
|
||||
code_action_kinds: Option<Vec<CodeActionKind>>,
|
||||
binary: LanguageServerBinary,
|
||||
cx: AsyncAppContext,
|
||||
|
@ -487,9 +488,9 @@ impl LanguageServer {
|
|||
executor: cx.background_executor().clone(),
|
||||
io_tasks: Mutex::new(Some((input_task, output_task))),
|
||||
output_done_rx: Mutex::new(Some(output_done_rx)),
|
||||
root_path: root_path.to_path_buf(),
|
||||
working_dir: working_dir.to_path_buf(),
|
||||
server: Arc::new(Mutex::new(server)),
|
||||
workspace_folders: Default::default(),
|
||||
registered_buffers: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -614,11 +615,12 @@ impl LanguageServer {
|
|||
}
|
||||
|
||||
pub fn default_initialize_params(&self, cx: &AppContext) -> InitializeParams {
|
||||
let root_uri = Url::from_file_path(&self.working_dir).unwrap();
|
||||
#[allow(deprecated)]
|
||||
InitializeParams {
|
||||
process_id: None,
|
||||
root_path: None,
|
||||
root_uri: None,
|
||||
root_uri: Some(root_uri.clone()),
|
||||
initialization_options: None,
|
||||
capabilities: ClientCapabilities {
|
||||
general: Some(GeneralClientCapabilities {
|
||||
|
@ -785,7 +787,10 @@ impl LanguageServer {
|
|||
}),
|
||||
},
|
||||
trace: None,
|
||||
workspace_folders: None,
|
||||
workspace_folders: Some(vec![WorkspaceFolder {
|
||||
uri: root_uri,
|
||||
name: Default::default(),
|
||||
}]),
|
||||
client_info: release_channel::ReleaseChannel::try_global(cx).map(|release_channel| {
|
||||
ClientInfo {
|
||||
name: release_channel.display_name().to_string(),
|
||||
|
@ -804,10 +809,16 @@ impl LanguageServer {
|
|||
/// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize)
|
||||
pub fn initialize(
|
||||
mut self,
|
||||
params: InitializeParams,
|
||||
initialize_params: Option<InitializeParams>,
|
||||
configuration: Arc<DidChangeConfigurationParams>,
|
||||
cx: &AppContext,
|
||||
) -> Task<Result<Arc<Self>>> {
|
||||
let params = if let Some(params) = initialize_params {
|
||||
params
|
||||
} else {
|
||||
self.default_initialize_params(cx)
|
||||
};
|
||||
|
||||
cx.spawn(|_| async move {
|
||||
let response = self.request::<request::Initialize>(params).await?;
|
||||
if let Some(info) = response.server_info {
|
||||
|
@ -1059,10 +1070,16 @@ impl LanguageServer {
|
|||
self.server_id
|
||||
}
|
||||
|
||||
/// Get the root path of the project the language server is running against.
|
||||
pub fn root_path(&self) -> &PathBuf {
|
||||
&self.root_path
|
||||
}
|
||||
|
||||
/// Language server's binary information.
|
||||
pub fn binary(&self) -> &LanguageServerBinary {
|
||||
&self.binary
|
||||
}
|
||||
|
||||
/// Sends a RPC request to the language server.
|
||||
///
|
||||
/// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#requestMessage)
|
||||
|
@ -1190,129 +1207,6 @@ impl LanguageServer {
|
|||
outbound_tx.try_send(message)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add new workspace folder to the list.
|
||||
pub fn add_workspace_folder(&self, uri: Url) {
|
||||
if self
|
||||
.capabilities()
|
||||
.workspace
|
||||
.and_then(|ws| {
|
||||
ws.workspace_folders.and_then(|folders| {
|
||||
folders
|
||||
.change_notifications
|
||||
.map(|caps| matches!(caps, OneOf::Left(false)))
|
||||
})
|
||||
})
|
||||
.unwrap_or(true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let is_new_folder = self.workspace_folders.lock().insert(uri.clone());
|
||||
if is_new_folder {
|
||||
let params = DidChangeWorkspaceFoldersParams {
|
||||
event: WorkspaceFoldersChangeEvent {
|
||||
added: vec![WorkspaceFolder {
|
||||
uri,
|
||||
name: String::default(),
|
||||
}],
|
||||
removed: vec![],
|
||||
},
|
||||
};
|
||||
self.notify::<DidChangeWorkspaceFolders>(¶ms).log_err();
|
||||
}
|
||||
}
|
||||
/// Add new workspace folder to the list.
|
||||
pub fn remove_workspace_folder(&self, uri: Url) {
|
||||
if self
|
||||
.capabilities()
|
||||
.workspace
|
||||
.and_then(|ws| {
|
||||
ws.workspace_folders.and_then(|folders| {
|
||||
folders
|
||||
.change_notifications
|
||||
.map(|caps| !matches!(caps, OneOf::Left(false)))
|
||||
})
|
||||
})
|
||||
.unwrap_or(true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
let was_removed = self.workspace_folders.lock().remove(&uri);
|
||||
if was_removed {
|
||||
let params = DidChangeWorkspaceFoldersParams {
|
||||
event: WorkspaceFoldersChangeEvent {
|
||||
added: vec![],
|
||||
removed: vec![WorkspaceFolder {
|
||||
uri,
|
||||
name: String::default(),
|
||||
}],
|
||||
},
|
||||
};
|
||||
self.notify::<DidChangeWorkspaceFolders>(¶ms).log_err();
|
||||
}
|
||||
}
|
||||
pub fn set_workspace_folders(&self, folders: BTreeSet<Url>) {
|
||||
let mut workspace_folders = self.workspace_folders.lock();
|
||||
let added: Vec<_> = folders
|
||||
.iter()
|
||||
.map(|uri| WorkspaceFolder {
|
||||
uri: uri.clone(),
|
||||
name: String::default(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let removed: Vec<_> = std::mem::replace(&mut *workspace_folders, folders)
|
||||
.into_iter()
|
||||
.map(|uri| WorkspaceFolder {
|
||||
uri: uri.clone(),
|
||||
name: String::default(),
|
||||
})
|
||||
.collect();
|
||||
let should_notify = !added.is_empty() || !removed.is_empty();
|
||||
|
||||
if should_notify {
|
||||
let params = DidChangeWorkspaceFoldersParams {
|
||||
event: WorkspaceFoldersChangeEvent { added, removed },
|
||||
};
|
||||
self.notify::<DidChangeWorkspaceFolders>(¶ms).log_err();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn workspace_folders(&self) -> impl Deref<Target = BTreeSet<Url>> + '_ {
|
||||
self.workspace_folders.lock()
|
||||
}
|
||||
|
||||
pub fn register_buffer(
|
||||
&self,
|
||||
buffer_id: BufferId,
|
||||
uri: Url,
|
||||
language_id: String,
|
||||
version: i32,
|
||||
initial_text: String,
|
||||
) {
|
||||
let previous_value = self
|
||||
.registered_buffers
|
||||
.lock()
|
||||
.insert(buffer_id, uri.clone());
|
||||
if previous_value.is_none() {
|
||||
self.notify::<notification::DidOpenTextDocument>(&DidOpenTextDocumentParams {
|
||||
text_document: TextDocumentItem::new(uri, language_id, version, initial_text),
|
||||
})
|
||||
.log_err();
|
||||
} else {
|
||||
debug_assert_eq!(previous_value, Some(uri));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unregister_buffer(&self, buffer_id: BufferId) {
|
||||
if let Some(path) = self.registered_buffers.lock().remove(&buffer_id) {
|
||||
self.notify::<notification::DidCloseTextDocument>(&DidCloseTextDocumentParams {
|
||||
text_document: TextDocumentIdentifier::new(path),
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for LanguageServer {
|
||||
|
@ -1394,6 +1288,8 @@ impl FakeLanguageServer {
|
|||
let (stdout_writer, stdout_reader) = async_pipe::pipe();
|
||||
let (notifications_tx, notifications_rx) = channel::unbounded();
|
||||
|
||||
let root = Self::root_path();
|
||||
|
||||
let server_name = LanguageServerName(name.clone().into());
|
||||
let process_name = Arc::from(name.as_str());
|
||||
let mut server = LanguageServer::new_internal(
|
||||
|
@ -1404,6 +1300,8 @@ impl FakeLanguageServer {
|
|||
None::<async_pipe::PipeReader>,
|
||||
Arc::new(Mutex::new(None)),
|
||||
None,
|
||||
root,
|
||||
root,
|
||||
None,
|
||||
binary.clone(),
|
||||
cx.clone(),
|
||||
|
@ -1421,6 +1319,8 @@ impl FakeLanguageServer {
|
|||
None::<async_pipe::PipeReader>,
|
||||
Arc::new(Mutex::new(None)),
|
||||
None,
|
||||
root,
|
||||
root,
|
||||
None,
|
||||
binary,
|
||||
cx.clone(),
|
||||
|
@ -1457,6 +1357,16 @@ impl FakeLanguageServer {
|
|||
|
||||
(server, fake)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn root_path() -> &'static Path {
|
||||
Path::new("C:\\")
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn root_path() -> &'static Path {
|
||||
Path::new("/")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
|
@ -1644,14 +1554,12 @@ mod tests {
|
|||
})
|
||||
.detach();
|
||||
|
||||
let initialize_params = None;
|
||||
let configuration = DidChangeConfigurationParams {
|
||||
settings: Default::default(),
|
||||
};
|
||||
let server = cx
|
||||
.update(|cx| {
|
||||
let params = server.default_initialize_params(cx);
|
||||
let configuration = DidChangeConfigurationParams {
|
||||
settings: Default::default(),
|
||||
};
|
||||
server.initialize(params, configuration.into(), cx)
|
||||
})
|
||||
.update(|cx| server.initialize(initialize_params, configuration.into(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
server
|
||||
|
|
|
@ -283,13 +283,13 @@ impl Prettier {
|
|||
)
|
||||
.context("prettier server creation")?;
|
||||
|
||||
let initialize_params = None;
|
||||
let configuration = lsp::DidChangeConfigurationParams {
|
||||
settings: Default::default(),
|
||||
};
|
||||
let server = cx
|
||||
.update(|cx| {
|
||||
let params = server.default_initialize_params(cx);
|
||||
let configuration = lsp::DidChangeConfigurationParams {
|
||||
settings: Default::default(),
|
||||
};
|
||||
executor.spawn(server.initialize(params, configuration.into(), cx))
|
||||
executor.spawn(server.initialize(initialize_params, configuration.into(), cx))
|
||||
})?
|
||||
.await
|
||||
.context("prettier server initialization")?;
|
||||
|
|
|
@ -43,7 +43,6 @@ log.workspace = true
|
|||
lsp.workspace = true
|
||||
node_runtime.workspace = true
|
||||
image.workspace = true
|
||||
once_cell.workspace = true
|
||||
parking_lot.workspace = true
|
||||
pathdiff.workspace = true
|
||||
paths.workspace = true
|
||||
|
|
|
@ -942,11 +942,9 @@ fn language_server_for_buffer(
|
|||
) -> Result<(Arc<CachedLspAdapter>, Arc<LanguageServer>)> {
|
||||
lsp_store
|
||||
.update(cx, |lsp_store, cx| {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
lsp_store
|
||||
.language_server_for_local_buffer(buffer, server_id, cx)
|
||||
.map(|(adapter, server)| (adapter.clone(), server.clone()))
|
||||
})
|
||||
lsp_store
|
||||
.language_server_for_local_buffer(buffer.read(cx), server_id, cx)
|
||||
.map(|(adapter, server)| (adapter.clone(), server.clone()))
|
||||
})?
|
||||
.ok_or_else(|| anyhow!("no language server found for buffer"))
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -40,7 +40,7 @@ pub struct PrettierStore {
|
|||
prettier_instances: HashMap<PathBuf, PrettierInstance>,
|
||||
}
|
||||
|
||||
pub(crate) enum PrettierStoreEvent {
|
||||
pub enum PrettierStoreEvent {
|
||||
LanguageServerRemoved(LanguageServerId),
|
||||
LanguageServerAdded {
|
||||
new_server_id: LanguageServerId,
|
||||
|
|
|
@ -9,7 +9,6 @@ pub mod lsp_ext_command;
|
|||
pub mod lsp_store;
|
||||
pub mod prettier_store;
|
||||
pub mod project_settings;
|
||||
mod project_tree;
|
||||
pub mod search;
|
||||
mod task_inventory;
|
||||
pub mod task_store;
|
||||
|
@ -475,7 +474,6 @@ pub struct DocumentHighlight {
|
|||
pub struct Symbol {
|
||||
pub language_server_name: LanguageServerName,
|
||||
pub source_worktree_id: WorktreeId,
|
||||
pub source_language_server_id: LanguageServerId,
|
||||
pub path: ProjectPath,
|
||||
pub label: CodeLabel,
|
||||
pub name: String,
|
||||
|
@ -1894,7 +1892,7 @@ impl Project {
|
|||
pub fn open_buffer(
|
||||
&mut self,
|
||||
path: impl Into<ProjectPath>,
|
||||
cx: &mut AppContext,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Model<Buffer>>> {
|
||||
if self.is_disconnected(cx) {
|
||||
return Task::ready(Err(anyhow!(ErrorCode::Disconnected)));
|
||||
|
@ -1909,11 +1907,11 @@ impl Project {
|
|||
pub fn open_buffer_with_lsp(
|
||||
&mut self,
|
||||
path: impl Into<ProjectPath>,
|
||||
cx: &mut AppContext,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<(Model<Buffer>, lsp_store::OpenLspBufferHandle)>> {
|
||||
let buffer = self.open_buffer(path, cx);
|
||||
let lsp_store = self.lsp_store().clone();
|
||||
cx.spawn(|mut cx| async move {
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let buffer = buffer.await?;
|
||||
let handle = lsp_store.update(&mut cx, |lsp_store, cx| {
|
||||
lsp_store.register_buffer_with_language_servers(&buffer, cx)
|
||||
|
@ -4149,25 +4147,14 @@ impl Project {
|
|||
self.lsp_store.read(cx).supplementary_language_servers()
|
||||
}
|
||||
|
||||
pub fn language_server_for_id(
|
||||
&self,
|
||||
id: LanguageServerId,
|
||||
cx: &AppContext,
|
||||
) -> Option<Arc<LanguageServer>> {
|
||||
self.lsp_store.read(cx).language_server_for_id(id)
|
||||
}
|
||||
|
||||
pub fn for_language_servers_for_local_buffer<R: 'static>(
|
||||
&self,
|
||||
buffer: &Buffer,
|
||||
callback: impl FnOnce(
|
||||
Box<dyn Iterator<Item = (&Arc<CachedLspAdapter>, &Arc<LanguageServer>)> + '_>,
|
||||
) -> R,
|
||||
cx: &mut AppContext,
|
||||
) -> R {
|
||||
self.lsp_store.update(cx, |this, cx| {
|
||||
callback(Box::new(this.language_servers_for_local_buffer(buffer, cx)))
|
||||
})
|
||||
pub fn language_servers_for_local_buffer<'a>(
|
||||
&'a self,
|
||||
buffer: &'a Buffer,
|
||||
cx: &'a AppContext,
|
||||
) -> impl Iterator<Item = (&'a Arc<CachedLspAdapter>, &'a Arc<LanguageServer>)> {
|
||||
self.lsp_store
|
||||
.read(cx)
|
||||
.language_servers_for_local_buffer(buffer, cx)
|
||||
}
|
||||
|
||||
pub fn buffer_store(&self) -> &Model<BufferStore> {
|
||||
|
|
|
@ -1749,12 +1749,6 @@ async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) {
|
|||
});
|
||||
})
|
||||
});
|
||||
let _rs_buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer_with_lsp("/dir/a.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let mut fake_rust_server_2 = fake_rust_servers.next().await.unwrap();
|
||||
assert_eq!(
|
||||
fake_rust_server_2
|
||||
|
@ -2579,28 +2573,25 @@ async fn test_definition(cx: &mut gpui::TestAppContext) {
|
|||
fs.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
"a.rs": "const fn a() { A }",
|
||||
"b.rs": "const y: i32 = crate::a()",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
fs.insert_tree(
|
||||
"/another_dir",
|
||||
json!({
|
||||
"a.rs": "const fn a() { A }"}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
|
||||
let project = Project::test(fs, ["/dir/b.rs".as_ref()], cx).await;
|
||||
|
||||
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
|
||||
language_registry.add(rust_lang());
|
||||
let mut fake_servers = language_registry.register_fake_lsp("Rust", FakeLspAdapter::default());
|
||||
|
||||
let (buffer, _handle) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer_with_lsp("/dir/b.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let fake_server = fake_servers.next().await.unwrap();
|
||||
fake_server.handle_request::<lsp::request::GotoDefinition, _, _>(|params, _| async move {
|
||||
let params = params.text_document_position_params;
|
||||
|
@ -2612,11 +2603,12 @@ async fn test_definition(cx: &mut gpui::TestAppContext) {
|
|||
|
||||
Ok(Some(lsp::GotoDefinitionResponse::Scalar(
|
||||
lsp::Location::new(
|
||||
lsp::Url::from_file_path("/another_dir/a.rs").unwrap(),
|
||||
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
|
||||
|
@ -2637,21 +2629,18 @@ async fn test_definition(cx: &mut gpui::TestAppContext) {
|
|||
.as_local()
|
||||
.unwrap()
|
||||
.abs_path(cx),
|
||||
Path::new("/another_dir/a.rs"),
|
||||
Path::new("/dir/a.rs"),
|
||||
);
|
||||
assert_eq!(definition.target.range.to_offset(target_buffer), 9..10);
|
||||
assert_eq!(
|
||||
list_worktrees(&project, cx),
|
||||
[
|
||||
("/another_dir/a.rs".as_ref(), false),
|
||||
("/dir".as_ref(), true)
|
||||
],
|
||||
[("/dir/a.rs".as_ref(), false), ("/dir/b.rs".as_ref(), true)],
|
||||
);
|
||||
|
||||
drop(definition);
|
||||
});
|
||||
cx.update(|cx| {
|
||||
assert_eq!(list_worktrees(&project, cx), [("/dir".as_ref(), true)]);
|
||||
assert_eq!(list_worktrees(&project, cx), [("/dir/b.rs".as_ref(), true)]);
|
||||
});
|
||||
|
||||
fn list_worktrees<'a>(
|
||||
|
|
|
@ -1,243 +0,0 @@
|
|||
//! This module defines a Project Tree.
|
||||
//!
|
||||
//! A Project Tree is responsible for determining where the roots of subprojects are located in a project.
|
||||
|
||||
mod path_trie;
|
||||
mod server_tree;
|
||||
|
||||
use std::{
|
||||
borrow::Borrow,
|
||||
collections::{hash_map::Entry, BTreeMap},
|
||||
ops::ControlFlow,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use collections::HashMap;
|
||||
use gpui::{AppContext, Context as _, EventEmitter, Model, ModelContext, Subscription};
|
||||
use language::{CachedLspAdapter, LspAdapterDelegate};
|
||||
use lsp::LanguageServerName;
|
||||
use path_trie::{LabelPresence, RootPathTrie, TriePath};
|
||||
use settings::{SettingsStore, WorktreeId};
|
||||
use worktree::{Event as WorktreeEvent, Worktree};
|
||||
|
||||
use crate::{
|
||||
worktree_store::{WorktreeStore, WorktreeStoreEvent},
|
||||
ProjectPath,
|
||||
};
|
||||
|
||||
pub(crate) use server_tree::{AdapterQuery, LanguageServerTree, LaunchDisposition};
|
||||
|
||||
struct WorktreeRoots {
|
||||
roots: RootPathTrie<LanguageServerName>,
|
||||
worktree_store: Model<WorktreeStore>,
|
||||
_worktree_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl WorktreeRoots {
|
||||
fn new(
|
||||
worktree_store: Model<WorktreeStore>,
|
||||
worktree: Model<Worktree>,
|
||||
cx: &mut AppContext,
|
||||
) -> Model<Self> {
|
||||
cx.new_model(|cx| Self {
|
||||
roots: RootPathTrie::new(),
|
||||
worktree_store,
|
||||
_worktree_subscription: cx.subscribe(&worktree, |this: &mut Self, _, event, cx| {
|
||||
match event {
|
||||
WorktreeEvent::UpdatedEntries(changes) => {
|
||||
for (path, _, kind) in changes.iter() {
|
||||
match kind {
|
||||
worktree::PathChange::Removed => {
|
||||
let path = TriePath::from(path.as_ref());
|
||||
this.roots.remove(&path);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
WorktreeEvent::UpdatedGitRepositories(_) => {}
|
||||
WorktreeEvent::DeletedEntry(entry_id) => {
|
||||
let Some(entry) = this.worktree_store.read(cx).entry_for_id(*entry_id, cx)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let path = TriePath::from(entry.path.as_ref());
|
||||
this.roots.remove(&path);
|
||||
}
|
||||
}
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ProjectTree {
|
||||
root_points: HashMap<WorktreeId, Model<WorktreeRoots>>,
|
||||
worktree_store: Model<WorktreeStore>,
|
||||
_subscriptions: [Subscription; 2],
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct AdapterWrapper(Arc<CachedLspAdapter>);
|
||||
impl PartialEq for AdapterWrapper {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.0.name.eq(&other.0.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for AdapterWrapper {}
|
||||
|
||||
impl std::hash::Hash for AdapterWrapper {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.0.name.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for AdapterWrapper {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.0.name.cmp(&other.0.name))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for AdapterWrapper {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.0.name.cmp(&other.0.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl Borrow<LanguageServerName> for AdapterWrapper {
|
||||
fn borrow(&self) -> &LanguageServerName {
|
||||
&self.0.name
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub(crate) enum ProjectTreeEvent {
|
||||
WorktreeRemoved(WorktreeId),
|
||||
Cleared,
|
||||
}
|
||||
|
||||
impl EventEmitter<ProjectTreeEvent> for ProjectTree {}
|
||||
|
||||
impl ProjectTree {
|
||||
pub(crate) fn new(worktree_store: Model<WorktreeStore>, cx: &mut AppContext) -> Model<Self> {
|
||||
cx.new_model(|cx| Self {
|
||||
root_points: Default::default(),
|
||||
_subscriptions: [
|
||||
cx.subscribe(&worktree_store, Self::on_worktree_store_event),
|
||||
cx.observe_global::<SettingsStore>(|this, cx| {
|
||||
for (_, roots) in &mut this.root_points {
|
||||
roots.update(cx, |worktree_roots, _| {
|
||||
worktree_roots.roots = RootPathTrie::new();
|
||||
})
|
||||
}
|
||||
cx.emit(ProjectTreeEvent::Cleared);
|
||||
}),
|
||||
],
|
||||
worktree_store,
|
||||
})
|
||||
}
|
||||
#[allow(clippy::mutable_key_type)]
|
||||
fn root_for_path(
|
||||
&mut self,
|
||||
ProjectPath { worktree_id, path }: ProjectPath,
|
||||
adapters: Vec<Arc<CachedLspAdapter>>,
|
||||
delegate: Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut AppContext,
|
||||
) -> BTreeMap<AdapterWrapper, ProjectPath> {
|
||||
debug_assert_eq!(delegate.worktree_id(), worktree_id);
|
||||
#[allow(clippy::mutable_key_type)]
|
||||
let mut roots = BTreeMap::from_iter(
|
||||
adapters
|
||||
.into_iter()
|
||||
.map(|adapter| (AdapterWrapper(adapter), (None, LabelPresence::KnownAbsent))),
|
||||
);
|
||||
let worktree_roots = match self.root_points.entry(worktree_id) {
|
||||
Entry::Occupied(occupied_entry) => occupied_entry.get().clone(),
|
||||
Entry::Vacant(vacant_entry) => {
|
||||
let Some(worktree) = self
|
||||
.worktree_store
|
||||
.read(cx)
|
||||
.worktree_for_id(worktree_id, cx)
|
||||
else {
|
||||
return Default::default();
|
||||
};
|
||||
let roots = WorktreeRoots::new(self.worktree_store.clone(), worktree, cx);
|
||||
vacant_entry.insert(roots).clone()
|
||||
}
|
||||
};
|
||||
|
||||
let key = TriePath::from(&*path);
|
||||
worktree_roots.update(cx, |this, _| {
|
||||
this.roots.walk(&key, &mut |path, labels| {
|
||||
for (label, presence) in labels {
|
||||
if let Some((marked_path, current_presence)) = roots.get_mut(label) {
|
||||
if *current_presence > *presence {
|
||||
debug_assert!(false, "RootPathTrie precondition violation; while walking the tree label presence is only allowed to increase");
|
||||
}
|
||||
*marked_path = Some(ProjectPath {worktree_id, path: path.clone()});
|
||||
*current_presence = *presence;
|
||||
}
|
||||
|
||||
}
|
||||
ControlFlow::Continue(())
|
||||
});
|
||||
});
|
||||
for (adapter, (root_path, presence)) in &mut roots {
|
||||
if *presence == LabelPresence::Present {
|
||||
continue;
|
||||
}
|
||||
|
||||
let depth = root_path
|
||||
.as_ref()
|
||||
.map(|root_path| {
|
||||
path.strip_prefix(&root_path.path)
|
||||
.unwrap()
|
||||
.components()
|
||||
.count()
|
||||
})
|
||||
.unwrap_or_else(|| path.components().count() + 1);
|
||||
|
||||
if depth > 0 {
|
||||
let root = adapter.0.find_project_root(&path, depth, &delegate);
|
||||
match root {
|
||||
Some(known_root) => worktree_roots.update(cx, |this, _| {
|
||||
let root = TriePath::from(&*known_root);
|
||||
this.roots
|
||||
.insert(&root, adapter.0.name(), LabelPresence::Present);
|
||||
*presence = LabelPresence::Present;
|
||||
*root_path = Some(ProjectPath {
|
||||
worktree_id,
|
||||
path: known_root,
|
||||
});
|
||||
}),
|
||||
None => worktree_roots.update(cx, |this, _| {
|
||||
this.roots
|
||||
.insert(&key, adapter.0.name(), LabelPresence::KnownAbsent);
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
roots
|
||||
.into_iter()
|
||||
.filter_map(|(k, (path, presence))| {
|
||||
let path = path?;
|
||||
presence.eq(&LabelPresence::Present).then(|| (k, path))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
fn on_worktree_store_event(
|
||||
&mut self,
|
||||
_: Model<WorktreeStore>,
|
||||
evt: &WorktreeStoreEvent,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
match evt {
|
||||
WorktreeStoreEvent::WorktreeRemoved(_, worktree_id) => {
|
||||
self.root_points.remove(&worktree_id);
|
||||
cx.emit(ProjectTreeEvent::WorktreeRemoved(*worktree_id));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,241 +0,0 @@
|
|||
use std::{
|
||||
collections::{btree_map::Entry, BTreeMap},
|
||||
ffi::OsStr,
|
||||
ops::ControlFlow,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
/// [RootPathTrie] is a workhorse of [super::ProjectTree]. It is responsible for determining the closest known project root for a given path.
|
||||
/// It also determines how much of a given path is unexplored, thus letting callers fill in that gap if needed.
|
||||
/// Conceptually, it allows one to annotate Worktree entries with arbitrary extra metadata and run closest-ancestor searches.
|
||||
///
|
||||
/// A path is unexplored when the closest ancestor of a path is not the path itself; that means that we have not yet ran the scan on that path.
|
||||
/// For example, if there's a project root at path `python/project` and we query for a path `python/project/subdir/another_subdir/file.py`, there is
|
||||
/// a known root at `python/project` and the unexplored part is `subdir/another_subdir` - we need to run a scan on these 2 directories.
|
||||
pub(super) struct RootPathTrie<Label> {
|
||||
worktree_relative_path: Arc<Path>,
|
||||
labels: BTreeMap<Label, LabelPresence>,
|
||||
children: BTreeMap<Arc<OsStr>, RootPathTrie<Label>>,
|
||||
}
|
||||
|
||||
/// Label presence is a marker that allows to optimize searches within [RootPathTrie]; node label can be:
|
||||
/// - Present; we know there's definitely a project root at this node and it is the only label of that kind on the path to the root of a worktree
|
||||
/// (none of it's ancestors or descendants can contain the same present label)
|
||||
/// - Known Absent - we know there's definitely no project root at this node and none of it's ancestors are Present (descendants can be present though!).
|
||||
/// - Forbidden - we know there's definitely no project root at this node and none of it's ancestors or descendants can be Present.
|
||||
/// The distinction is there to optimize searching; when we encounter a node with unknown status, we don't need to look at it's full path
|
||||
/// to the root of the worktree; it's sufficient to explore only the path between last node with a KnownAbsent state and the directory of a path, since we run searches
|
||||
/// from the leaf up to the root of the worktree. When any of the ancestors is forbidden, we don't need to look at the node or its ancestors.
|
||||
/// When there's a present labeled node on the path to the root, we don't need to ask the adapter to run the search at all.
|
||||
///
|
||||
/// In practical terms, it means that by storing label presence we don't need to do a project discovery on a given folder more than once
|
||||
/// (unless the node is invalidated, which can happen when FS entries are renamed/removed).
|
||||
///
|
||||
/// Storing project absence allows us to recognize which paths have already been scanned for a project root unsuccessfully. This way we don't need to run
|
||||
/// such scan more than once.
|
||||
#[derive(Clone, Copy, Debug, PartialOrd, PartialEq, Ord, Eq)]
|
||||
pub(super) enum LabelPresence {
|
||||
KnownAbsent,
|
||||
Present,
|
||||
}
|
||||
|
||||
impl<Label: Ord + Clone> RootPathTrie<Label> {
|
||||
pub(super) fn new() -> Self {
|
||||
Self::new_with_key(Arc::from(Path::new("")))
|
||||
}
|
||||
fn new_with_key(worktree_relative_path: Arc<Path>) -> Self {
|
||||
RootPathTrie {
|
||||
worktree_relative_path,
|
||||
labels: Default::default(),
|
||||
children: Default::default(),
|
||||
}
|
||||
}
|
||||
// Internal implementation of inner that allows one to visit descendants of insertion point for a node.
|
||||
fn insert_inner(
|
||||
&mut self,
|
||||
path: &TriePath,
|
||||
value: Label,
|
||||
presence: LabelPresence,
|
||||
) -> &mut Self {
|
||||
let mut current = self;
|
||||
|
||||
let mut path_so_far = PathBuf::new();
|
||||
for key in path.0.iter() {
|
||||
path_so_far.push(Path::new(key));
|
||||
current = match current.children.entry(key.clone()) {
|
||||
Entry::Vacant(vacant_entry) => vacant_entry
|
||||
.insert(RootPathTrie::new_with_key(Arc::from(path_so_far.as_path()))),
|
||||
Entry::Occupied(occupied_entry) => occupied_entry.into_mut(),
|
||||
};
|
||||
}
|
||||
let _previous_value = current.labels.insert(value, presence);
|
||||
debug_assert_eq!(_previous_value, None);
|
||||
current
|
||||
}
|
||||
pub(super) fn insert(&mut self, path: &TriePath, value: Label, presence: LabelPresence) {
|
||||
self.insert_inner(path, value, presence);
|
||||
}
|
||||
|
||||
pub(super) fn walk<'a>(
|
||||
&'a self,
|
||||
path: &TriePath,
|
||||
callback: &mut dyn for<'b> FnMut(
|
||||
&'b Arc<Path>,
|
||||
&'a BTreeMap<Label, LabelPresence>,
|
||||
) -> ControlFlow<()>,
|
||||
) {
|
||||
let mut current = self;
|
||||
for key in path.0.iter() {
|
||||
if !current.labels.is_empty() {
|
||||
if (callback)(¤t.worktree_relative_path, ¤t.labels).is_break() {
|
||||
return;
|
||||
};
|
||||
}
|
||||
current = match current.children.get(key) {
|
||||
Some(child) => child,
|
||||
None => return,
|
||||
};
|
||||
}
|
||||
if !current.labels.is_empty() {
|
||||
(callback)(¤t.worktree_relative_path, ¤t.labels);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn remove(&mut self, path: &TriePath) {
|
||||
debug_assert_ne!(path.0.len(), 0);
|
||||
let mut current = self;
|
||||
for path in path.0.iter().take(path.0.len().saturating_sub(1)) {
|
||||
current = match current.children.get_mut(path) {
|
||||
Some(child) => child,
|
||||
None => return,
|
||||
};
|
||||
}
|
||||
if let Some(final_entry_name) = path.0.last() {
|
||||
current.children.remove(final_entry_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// [TriePath] is a [Path] preprocessed for amortizing the cost of doing multiple lookups in distinct [RootPathTrie]s.
|
||||
#[derive(Clone)]
|
||||
pub(super) struct TriePath(Arc<[Arc<OsStr>]>);
|
||||
|
||||
impl From<&Path> for TriePath {
|
||||
fn from(value: &Path) -> Self {
|
||||
TriePath(value.components().map(|c| c.as_os_str().into()).collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_insert_and_lookup() {
|
||||
let mut trie = RootPathTrie::<()>::new();
|
||||
trie.insert(
|
||||
&TriePath::from(Path::new("a/b/c")),
|
||||
(),
|
||||
LabelPresence::Present,
|
||||
);
|
||||
|
||||
trie.walk(&TriePath::from(Path::new("a/b/c")), &mut |path, nodes| {
|
||||
assert_eq!(nodes.get(&()), Some(&LabelPresence::Present));
|
||||
assert_eq!(path.as_ref(), Path::new("a/b/c"));
|
||||
ControlFlow::Continue(())
|
||||
});
|
||||
// Now let's annotate a parent with "Known missing" node.
|
||||
trie.insert(
|
||||
&TriePath::from(Path::new("a")),
|
||||
(),
|
||||
LabelPresence::KnownAbsent,
|
||||
);
|
||||
|
||||
// Ensure that we walk from the root to the leaf.
|
||||
let mut visited_paths = BTreeSet::new();
|
||||
trie.walk(&TriePath::from(Path::new("a/b/c")), &mut |path, nodes| {
|
||||
if path.as_ref() == Path::new("a/b/c") {
|
||||
assert_eq!(
|
||||
visited_paths,
|
||||
BTreeSet::from_iter([Arc::from(Path::new("a/"))])
|
||||
);
|
||||
assert_eq!(nodes.get(&()), Some(&LabelPresence::Present));
|
||||
} else if path.as_ref() == Path::new("a/") {
|
||||
assert!(visited_paths.is_empty());
|
||||
assert_eq!(nodes.get(&()), Some(&LabelPresence::KnownAbsent));
|
||||
} else {
|
||||
panic!("Unknown path");
|
||||
}
|
||||
// Assert that we only ever visit a path once.
|
||||
assert!(visited_paths.insert(path.clone()));
|
||||
ControlFlow::Continue(())
|
||||
});
|
||||
|
||||
// One can also pass a path whose prefix is in the tree, but not that path itself.
|
||||
let mut visited_paths = BTreeSet::new();
|
||||
trie.walk(
|
||||
&TriePath::from(Path::new("a/b/c/d/e/f/g")),
|
||||
&mut |path, nodes| {
|
||||
if path.as_ref() == Path::new("a/b/c") {
|
||||
assert_eq!(
|
||||
visited_paths,
|
||||
BTreeSet::from_iter([Arc::from(Path::new("a/"))])
|
||||
);
|
||||
assert_eq!(nodes.get(&()), Some(&LabelPresence::Present));
|
||||
} else if path.as_ref() == Path::new("a/") {
|
||||
assert!(visited_paths.is_empty());
|
||||
assert_eq!(nodes.get(&()), Some(&LabelPresence::KnownAbsent));
|
||||
} else {
|
||||
panic!("Unknown path");
|
||||
}
|
||||
// Assert that we only ever visit a path once.
|
||||
assert!(visited_paths.insert(path.clone()));
|
||||
ControlFlow::Continue(())
|
||||
},
|
||||
);
|
||||
|
||||
// Test breaking from the tree-walk.
|
||||
let mut visited_paths = BTreeSet::new();
|
||||
trie.walk(&TriePath::from(Path::new("a/b/c")), &mut |path, nodes| {
|
||||
if path.as_ref() == Path::new("a/") {
|
||||
assert!(visited_paths.is_empty());
|
||||
assert_eq!(nodes.get(&()), Some(&LabelPresence::KnownAbsent));
|
||||
} else {
|
||||
panic!("Unknown path");
|
||||
}
|
||||
// Assert that we only ever visit a path once.
|
||||
assert!(visited_paths.insert(path.clone()));
|
||||
ControlFlow::Break(())
|
||||
});
|
||||
assert_eq!(visited_paths.len(), 1);
|
||||
|
||||
// Entry removal.
|
||||
trie.insert(
|
||||
&TriePath::from(Path::new("a/b")),
|
||||
(),
|
||||
LabelPresence::KnownAbsent,
|
||||
);
|
||||
let mut visited_paths = BTreeSet::new();
|
||||
trie.walk(&TriePath::from(Path::new("a/b/c")), &mut |path, _nodes| {
|
||||
// Assert that we only ever visit a path once.
|
||||
assert!(visited_paths.insert(path.clone()));
|
||||
ControlFlow::Continue(())
|
||||
});
|
||||
assert_eq!(visited_paths.len(), 3);
|
||||
trie.remove(&TriePath::from(Path::new("a/b/")));
|
||||
let mut visited_paths = BTreeSet::new();
|
||||
trie.walk(&TriePath::from(Path::new("a/b/c")), &mut |path, _nodes| {
|
||||
// Assert that we only ever visit a path once.
|
||||
assert!(visited_paths.insert(path.clone()));
|
||||
ControlFlow::Continue(())
|
||||
});
|
||||
assert_eq!(visited_paths.len(), 1);
|
||||
assert_eq!(
|
||||
visited_paths.into_iter().next().unwrap().as_ref(),
|
||||
Path::new("a/")
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,462 +0,0 @@
|
|||
//! This module defines an LSP Tree.
|
||||
//!
|
||||
//! An LSP Tree is responsible for determining which language servers apply to a given project path.
|
||||
//!
|
||||
//! ## RPC
|
||||
//! LSP Tree is transparent to RPC peers; when clients ask host to spawn a new language server, the host will perform LSP Tree lookup for provided path; it may decide
|
||||
//! to reuse existing language server. The client maintains it's own LSP Tree that is a subset of host LSP Tree. Done this way, the client does not need to
|
||||
//! ask about suitable language server for each path it interacts with; it can resolve most of the queries locally.
|
||||
//! This module defines a Project Tree.
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
path::Path,
|
||||
sync::{Arc, Weak},
|
||||
};
|
||||
|
||||
use collections::{HashMap, IndexMap};
|
||||
use gpui::{AppContext, Context as _, Model, Subscription};
|
||||
use language::{
|
||||
language_settings::AllLanguageSettings, Attach, LanguageName, LanguageRegistry,
|
||||
LspAdapterDelegate,
|
||||
};
|
||||
use lsp::LanguageServerName;
|
||||
use once_cell::sync::OnceCell;
|
||||
use settings::{Settings, SettingsLocation, WorktreeId};
|
||||
use util::maybe;
|
||||
|
||||
use crate::{project_settings::LspSettings, LanguageServerId, ProjectPath};
|
||||
|
||||
use super::{AdapterWrapper, ProjectTree, ProjectTreeEvent};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct ServersForWorktree {
|
||||
roots: BTreeMap<
|
||||
Arc<Path>,
|
||||
BTreeMap<LanguageServerName, (Arc<InnerTreeNode>, BTreeSet<LanguageName>)>,
|
||||
>,
|
||||
}
|
||||
|
||||
pub struct LanguageServerTree {
|
||||
project_tree: Model<ProjectTree>,
|
||||
instances: BTreeMap<WorktreeId, ServersForWorktree>,
|
||||
attach_kind_cache: HashMap<LanguageServerName, Attach>,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
_subscriptions: Subscription,
|
||||
}
|
||||
|
||||
/// A node in language server tree represents either:
|
||||
/// - A language server that has already been initialized/updated for a given project
|
||||
/// - A soon-to-be-initialized language server.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct LanguageServerTreeNode(Weak<InnerTreeNode>);
|
||||
|
||||
/// Describes a request to launch a language server.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct LaunchDisposition<'a> {
|
||||
pub(crate) server_name: &'a LanguageServerName,
|
||||
pub(crate) attach: Attach,
|
||||
pub(crate) path: ProjectPath,
|
||||
pub(crate) settings: Arc<LspSettings>,
|
||||
}
|
||||
|
||||
impl<'a> From<&'a InnerTreeNode> for LaunchDisposition<'a> {
|
||||
fn from(value: &'a InnerTreeNode) -> Self {
|
||||
LaunchDisposition {
|
||||
server_name: &value.name,
|
||||
attach: value.attach,
|
||||
path: value.path.clone(),
|
||||
settings: value.settings.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl LanguageServerTreeNode {
|
||||
/// Returns a language server ID for this node if there is one.
|
||||
/// Returns None if this node has not been initialized yet or it is no longer in the tree.
|
||||
pub(crate) fn server_id(&self) -> Option<LanguageServerId> {
|
||||
self.0.upgrade()?.id.get().copied()
|
||||
}
|
||||
/// Returns a language server ID for this node if it has already been initialized; otherwise runs the provided closure to initialize the language server node in a tree.
|
||||
/// May return None if the node no longer belongs to the server tree it was created in.
|
||||
pub(crate) fn server_id_or_init(
|
||||
&self,
|
||||
init: impl FnOnce(LaunchDisposition) -> LanguageServerId,
|
||||
) -> Option<LanguageServerId> {
|
||||
self.server_id_or_try_init(|disposition| Ok(init(disposition)))
|
||||
}
|
||||
fn server_id_or_try_init(
|
||||
&self,
|
||||
init: impl FnOnce(LaunchDisposition) -> Result<LanguageServerId, ()>,
|
||||
) -> Option<LanguageServerId> {
|
||||
let this = self.0.upgrade()?;
|
||||
this.id
|
||||
.get_or_try_init(|| init(LaunchDisposition::from(&*this)))
|
||||
.ok()
|
||||
.copied()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Weak<InnerTreeNode>> for LanguageServerTreeNode {
|
||||
fn from(weak: Weak<InnerTreeNode>) -> Self {
|
||||
LanguageServerTreeNode(weak)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct InnerTreeNode {
|
||||
id: OnceCell<LanguageServerId>,
|
||||
name: LanguageServerName,
|
||||
attach: Attach,
|
||||
path: ProjectPath,
|
||||
settings: Arc<LspSettings>,
|
||||
}
|
||||
|
||||
impl InnerTreeNode {
|
||||
fn new(
|
||||
name: LanguageServerName,
|
||||
attach: Attach,
|
||||
path: ProjectPath,
|
||||
settings: impl Into<Arc<LspSettings>>,
|
||||
) -> Self {
|
||||
InnerTreeNode {
|
||||
id: Default::default(),
|
||||
name,
|
||||
attach,
|
||||
path,
|
||||
settings: settings.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Determines how the list of adapters to query should be constructed.
|
||||
pub(crate) enum AdapterQuery<'a> {
|
||||
/// Search for roots of all adapters associated with a given language name.
|
||||
Language(&'a LanguageName),
|
||||
/// Search for roots of adapter with a given name.
|
||||
Adapter(&'a LanguageServerName),
|
||||
}
|
||||
|
||||
impl LanguageServerTree {
|
||||
pub(crate) fn new(
|
||||
project_tree: Model<ProjectTree>,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
cx: &mut AppContext,
|
||||
) -> Model<Self> {
|
||||
cx.new_model(|cx| Self {
|
||||
_subscriptions: cx.subscribe(
|
||||
&project_tree,
|
||||
|_: &mut Self, _, event, _| {
|
||||
if event == &ProjectTreeEvent::Cleared {}
|
||||
},
|
||||
),
|
||||
project_tree,
|
||||
instances: Default::default(),
|
||||
attach_kind_cache: Default::default(),
|
||||
languages,
|
||||
})
|
||||
}
|
||||
/// Memoize calls to attach_kind on LspAdapter (which might be a WASM extension, thus ~expensive to call).
|
||||
fn attach_kind(&mut self, adapter: &AdapterWrapper) -> Attach {
|
||||
*self
|
||||
.attach_kind_cache
|
||||
.entry(adapter.0.name.clone())
|
||||
.or_insert_with(|| adapter.0.attach_kind())
|
||||
}
|
||||
|
||||
/// Get all language server root points for a given path and language; the language servers might already be initialized at a given path.
|
||||
pub(crate) fn get<'a>(
|
||||
&'a mut self,
|
||||
path: ProjectPath,
|
||||
query: AdapterQuery<'_>,
|
||||
delegate: Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut AppContext,
|
||||
) -> impl Iterator<Item = LanguageServerTreeNode> + 'a {
|
||||
let settings_location = SettingsLocation {
|
||||
worktree_id: path.worktree_id,
|
||||
path: &path.path,
|
||||
};
|
||||
let adapters = match query {
|
||||
AdapterQuery::Language(language_name) => {
|
||||
self.adapters_for_language(settings_location, language_name, cx)
|
||||
}
|
||||
AdapterQuery::Adapter(language_server_name) => IndexMap::from_iter(
|
||||
self.adapter_for_name(language_server_name)
|
||||
.map(|adapter| (adapter, (LspSettings::default(), BTreeSet::new()))),
|
||||
),
|
||||
};
|
||||
self.get_with_adapters(path, adapters, delegate, cx)
|
||||
}
|
||||
|
||||
fn get_with_adapters<'a>(
|
||||
&'a mut self,
|
||||
path: ProjectPath,
|
||||
adapters: IndexMap<AdapterWrapper, (LspSettings, BTreeSet<LanguageName>)>,
|
||||
delegate: Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut AppContext,
|
||||
) -> impl Iterator<Item = LanguageServerTreeNode> + 'a {
|
||||
let worktree_id = path.worktree_id;
|
||||
#[allow(clippy::mutable_key_type)]
|
||||
let mut roots = self.project_tree.update(cx, |this, cx| {
|
||||
this.root_for_path(
|
||||
path,
|
||||
adapters
|
||||
.iter()
|
||||
.map(|(adapter, _)| adapter.0.clone())
|
||||
.collect(),
|
||||
delegate,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let mut root_path = None;
|
||||
// Backwards-compat: Fill in any adapters for which we did not detect the root as having the project root at the root of a worktree.
|
||||
for (adapter, _) in adapters.iter() {
|
||||
roots.entry(adapter.clone()).or_insert_with(|| {
|
||||
root_path
|
||||
.get_or_insert_with(|| ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from("".as_ref()),
|
||||
})
|
||||
.clone()
|
||||
});
|
||||
}
|
||||
|
||||
roots.into_iter().filter_map(move |(adapter, root_path)| {
|
||||
let attach = self.attach_kind(&adapter);
|
||||
let (settings, new_languages) = adapters.get(&adapter).cloned()?;
|
||||
let inner_node = self
|
||||
.instances
|
||||
.entry(root_path.worktree_id)
|
||||
.or_default()
|
||||
.roots
|
||||
.entry(root_path.path.clone())
|
||||
.or_default()
|
||||
.entry(adapter.0.name.clone());
|
||||
let (node, languages) = inner_node.or_insert_with(move || {
|
||||
(
|
||||
Arc::new(InnerTreeNode::new(
|
||||
adapter.0.name(),
|
||||
attach,
|
||||
root_path,
|
||||
settings,
|
||||
)),
|
||||
Default::default(),
|
||||
)
|
||||
});
|
||||
languages.extend(new_languages);
|
||||
Some(Arc::downgrade(&node).into())
|
||||
})
|
||||
}
|
||||
|
||||
fn adapter_for_name(&self, name: &LanguageServerName) -> Option<AdapterWrapper> {
|
||||
self.languages.adapter_for_name(name).map(AdapterWrapper)
|
||||
}
|
||||
|
||||
fn adapters_for_language(
|
||||
&self,
|
||||
settings_location: SettingsLocation,
|
||||
language_name: &LanguageName,
|
||||
cx: &AppContext,
|
||||
) -> IndexMap<AdapterWrapper, (LspSettings, BTreeSet<LanguageName>)> {
|
||||
let settings = AllLanguageSettings::get(Some(settings_location), cx).language(
|
||||
Some(settings_location),
|
||||
Some(language_name),
|
||||
cx,
|
||||
);
|
||||
if !settings.enable_language_server {
|
||||
return Default::default();
|
||||
}
|
||||
let available_lsp_adapters = self.languages.lsp_adapters(&language_name);
|
||||
let available_language_servers = available_lsp_adapters
|
||||
.iter()
|
||||
.map(|lsp_adapter| lsp_adapter.name.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let desired_language_servers =
|
||||
settings.customized_language_servers(&available_language_servers);
|
||||
let adapters_with_settings = desired_language_servers
|
||||
.into_iter()
|
||||
.filter_map(|desired_adapter| {
|
||||
let adapter = if let Some(adapter) = available_lsp_adapters
|
||||
.iter()
|
||||
.find(|adapter| adapter.name == desired_adapter)
|
||||
{
|
||||
Some(adapter.clone())
|
||||
} else if let Some(adapter) =
|
||||
self.languages.load_available_lsp_adapter(&desired_adapter)
|
||||
{
|
||||
self.languages
|
||||
.register_lsp_adapter(language_name.clone(), adapter.adapter.clone());
|
||||
Some(adapter)
|
||||
} else {
|
||||
None
|
||||
}?;
|
||||
let adapter_settings = crate::lsp_store::language_server_settings_for(
|
||||
settings_location,
|
||||
&adapter.name,
|
||||
cx,
|
||||
)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
Some((
|
||||
AdapterWrapper(adapter),
|
||||
(
|
||||
adapter_settings,
|
||||
BTreeSet::from_iter([language_name.clone()]),
|
||||
),
|
||||
))
|
||||
})
|
||||
.collect::<IndexMap<_, _>>();
|
||||
// After starting all the language servers, reorder them to reflect the desired order
|
||||
// based on the settings.
|
||||
//
|
||||
// This is done, in part, to ensure that language servers loaded at different points
|
||||
// (e.g., native vs extension) still end up in the right order at the end, rather than
|
||||
// it being based on which language server happened to be loaded in first.
|
||||
self.languages.reorder_language_servers(
|
||||
&language_name,
|
||||
adapters_with_settings
|
||||
.keys()
|
||||
.map(|wrapper| wrapper.0.clone())
|
||||
.collect(),
|
||||
);
|
||||
|
||||
adapters_with_settings
|
||||
}
|
||||
|
||||
pub(crate) fn on_settings_changed(
|
||||
&mut self,
|
||||
get_delegate: &mut dyn FnMut(
|
||||
WorktreeId,
|
||||
&mut AppContext,
|
||||
) -> Option<Arc<dyn LspAdapterDelegate>>,
|
||||
spawn_language_server: &mut dyn FnMut(
|
||||
LaunchDisposition,
|
||||
&mut AppContext,
|
||||
) -> LanguageServerId,
|
||||
on_language_server_removed: &mut dyn FnMut(LanguageServerId),
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
// Settings are checked at query time. Thus, to avoid messing with inference of applicable settings, we're just going to clear ourselves and let the next query repopulate.
|
||||
// We're going to optimistically re-run the queries and re-assign the same language server id when a language server still exists at a given tree node.
|
||||
let old_instances = std::mem::take(&mut self.instances);
|
||||
let old_attach_kinds = std::mem::take(&mut self.attach_kind_cache);
|
||||
|
||||
let mut referenced_instances = BTreeSet::new();
|
||||
// Re-map the old tree onto a new one. In the process we'll get a list of servers we have to shut down.
|
||||
let mut all_instances = BTreeSet::new();
|
||||
|
||||
for (worktree_id, servers) in &old_instances {
|
||||
// Record all initialized node ids.
|
||||
all_instances.extend(servers.roots.values().flat_map(|servers_at_node| {
|
||||
servers_at_node
|
||||
.values()
|
||||
.filter_map(|(server_node, _)| server_node.id.get().copied())
|
||||
}));
|
||||
let Some(delegate) = get_delegate(*worktree_id, cx) else {
|
||||
// If worktree is no longer around, we're just going to shut down all of the language servers (since they've been added to all_instances).
|
||||
continue;
|
||||
};
|
||||
|
||||
for (path, servers_for_path) in &servers.roots {
|
||||
for (server_name, (_, languages)) in servers_for_path {
|
||||
let settings_location = SettingsLocation {
|
||||
worktree_id: *worktree_id,
|
||||
path: &path,
|
||||
};
|
||||
// Verify which of the previous languages still have this server enabled.
|
||||
|
||||
let mut adapter_with_settings = IndexMap::default();
|
||||
|
||||
for language_name in languages {
|
||||
self.adapters_for_language(settings_location, language_name, cx)
|
||||
.into_iter()
|
||||
.for_each(|(lsp_adapter, lsp_settings)| {
|
||||
if &lsp_adapter.0.name() != server_name {
|
||||
return;
|
||||
}
|
||||
adapter_with_settings
|
||||
.entry(lsp_adapter)
|
||||
.and_modify(|x: &mut (_, BTreeSet<LanguageName>)| {
|
||||
x.1.extend(lsp_settings.1.clone())
|
||||
})
|
||||
.or_insert(lsp_settings);
|
||||
});
|
||||
}
|
||||
|
||||
if adapter_with_settings.is_empty() {
|
||||
// Since all languages that have had this server enabled are now disabled, we can remove the server entirely.
|
||||
continue;
|
||||
};
|
||||
|
||||
for new_node in self.get_with_adapters(
|
||||
ProjectPath {
|
||||
path: path.clone(),
|
||||
worktree_id: *worktree_id,
|
||||
},
|
||||
adapter_with_settings,
|
||||
delegate.clone(),
|
||||
cx,
|
||||
) {
|
||||
new_node.server_id_or_try_init(|disposition| {
|
||||
let Some((existing_node, _)) = servers
|
||||
.roots
|
||||
.get(&disposition.path.path)
|
||||
.and_then(|roots| roots.get(disposition.server_name))
|
||||
.filter(|(old_node, _)| {
|
||||
old_attach_kinds.get(disposition.server_name).map_or(
|
||||
false,
|
||||
|old_attach| {
|
||||
disposition.attach == *old_attach
|
||||
&& disposition.settings == old_node.settings
|
||||
},
|
||||
)
|
||||
})
|
||||
else {
|
||||
return Ok(spawn_language_server(disposition, cx));
|
||||
};
|
||||
if let Some(id) = existing_node.id.get().copied() {
|
||||
// If we have a node with ID assigned (and it's parameters match `disposition`), reuse the id.
|
||||
referenced_instances.insert(id);
|
||||
Ok(id)
|
||||
} else {
|
||||
// Otherwise, if we do have a node but it does not have an ID assigned, keep it that way.
|
||||
Err(())
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for server_to_remove in all_instances.difference(&referenced_instances) {
|
||||
on_language_server_removed(*server_to_remove);
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates nodes in language server tree in place, changing the ID of initialized nodes.
|
||||
pub(crate) fn restart_language_servers(
|
||||
&mut self,
|
||||
worktree_id: WorktreeId,
|
||||
ids: BTreeSet<LanguageServerId>,
|
||||
restart_callback: &mut dyn FnMut(LanguageServerId, LaunchDisposition) -> LanguageServerId,
|
||||
) {
|
||||
maybe! {{
|
||||
for (_, nodes) in &mut self.instances.get_mut(&worktree_id)?.roots {
|
||||
for (_, (node, _)) in nodes {
|
||||
let Some(old_server_id) = node.id.get().copied() else {
|
||||
continue;
|
||||
};
|
||||
if !ids.contains(&old_server_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let new_id = restart_callback(old_server_id, LaunchDisposition::from(&**node));
|
||||
|
||||
*node = Arc::new(InnerTreeNode::new(node.name.clone(), node.attach, node.path.clone(), node.settings.clone()));
|
||||
node.id.set(new_id).expect("The id to be unset after clearing the node.");
|
||||
}
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -765,7 +765,6 @@ message Symbol {
|
|||
PointUtf16 start = 7;
|
||||
PointUtf16 end = 8;
|
||||
bytes signature = 9;
|
||||
uint64 language_server_id = 10;
|
||||
}
|
||||
|
||||
message OpenBufferForSymbol {
|
||||
|
|
|
@ -16,4 +16,4 @@ doctest = false
|
|||
[dependencies]
|
||||
syn = "1.0.72"
|
||||
quote = "1.0.9"
|
||||
proc-macro2 = "1.0.93"
|
||||
proc-macro2 = "1.0.66"
|
||||
|
|
|
@ -13,7 +13,7 @@ path = "src/ui_macros.rs"
|
|||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "1.0.93"
|
||||
proc-macro2 = "1.0.66"
|
||||
quote = "1.0.9"
|
||||
syn = { version = "1.0.72", features = ["full", "extra-traits"] }
|
||||
convert_case.workspace = true
|
||||
|
|
|
@ -95,17 +95,26 @@ impl Render for QuickActionBar {
|
|||
show_git_blame_gutter,
|
||||
auto_signature_help_enabled,
|
||||
inline_completions_enabled,
|
||||
) = editor.update(cx, |editor, cx| {
|
||||
) = {
|
||||
let editor = editor.read(cx);
|
||||
let selection_menu_enabled = editor.selection_menu_enabled(cx);
|
||||
let inlay_hints_enabled = editor.inlay_hints_enabled();
|
||||
let supports_inlay_hints = editor.supports_inlay_hints(cx);
|
||||
let git_blame_inline_enabled = editor.git_blame_inline_enabled();
|
||||
let show_git_blame_gutter = editor.show_git_blame_gutter();
|
||||
let auto_signature_help_enabled = editor.auto_signature_help_enabled(cx);
|
||||
let inline_completions_enabled = editor.inline_completions_enabled(cx);
|
||||
|
||||
(
|
||||
editor.selection_menu_enabled(cx),
|
||||
editor.inlay_hints_enabled(),
|
||||
editor.supports_inlay_hints(cx),
|
||||
editor.git_blame_inline_enabled(),
|
||||
editor.show_git_blame_gutter(),
|
||||
editor.auto_signature_help_enabled(cx),
|
||||
editor.inline_completions_enabled(cx),
|
||||
selection_menu_enabled,
|
||||
inlay_hints_enabled,
|
||||
supports_inlay_hints,
|
||||
git_blame_inline_enabled,
|
||||
show_git_blame_gutter,
|
||||
auto_signature_help_enabled,
|
||||
inline_completions_enabled,
|
||||
)
|
||||
});
|
||||
};
|
||||
|
||||
let focus_handle = editor.read(cx).focus_handle(cx);
|
||||
|
||||
|
@ -441,19 +450,16 @@ impl ToolbarItemView for QuickActionBar {
|
|||
|
||||
if let Some(editor) = active_item.downcast::<Editor>() {
|
||||
let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
|
||||
let mut supports_inlay_hints =
|
||||
editor.update(cx, |this, cx| this.supports_inlay_hints(cx));
|
||||
let mut supports_inlay_hints = editor.read(cx).supports_inlay_hints(cx);
|
||||
self._inlay_hints_enabled_subscription =
|
||||
Some(cx.observe(&editor, move |_, editor, cx| {
|
||||
let mut should_notify = false;
|
||||
editor.update(cx, |editor, cx| {
|
||||
let new_inlay_hints_enabled = editor.inlay_hints_enabled();
|
||||
let new_supports_inlay_hints = editor.supports_inlay_hints(cx);
|
||||
should_notify = inlay_hints_enabled != new_inlay_hints_enabled
|
||||
|| supports_inlay_hints != new_supports_inlay_hints;
|
||||
inlay_hints_enabled = new_inlay_hints_enabled;
|
||||
supports_inlay_hints = new_supports_inlay_hints;
|
||||
});
|
||||
let editor = editor.read(cx);
|
||||
let new_inlay_hints_enabled = editor.inlay_hints_enabled();
|
||||
let new_supports_inlay_hints = editor.supports_inlay_hints(cx);
|
||||
let should_notify = inlay_hints_enabled != new_inlay_hints_enabled
|
||||
|| supports_inlay_hints != new_supports_inlay_hints;
|
||||
inlay_hints_enabled = new_inlay_hints_enabled;
|
||||
supports_inlay_hints = new_supports_inlay_hints;
|
||||
if should_notify {
|
||||
cx.notify()
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue