project: Allow running multiple instances of a single language server within a single worktree (#23473)
This PR introduces a new entity called Project Tree which is responsible for finding subprojects within a worktree; a subproject is a language-specific subset of a worktree which should be accurately tracked on the language server side. We'll have an ability to set multiple disjoint workspaceFolders on language server side OR spawn multiple instances of a single language server (which will be the case with e.g. Python language servers, as they need to interact with multiple disjoint virtual environments). Project Tree assumes that projects of the same LspAdapter kind cannot overlap. Additionally project nesting is not allowed within the scope of a single LspAdapter. Closes https://github.com/zed-industries/zed/issues/5108 Re-lands #22182 which I had to revert due to merging it into todays Preview. Release Notes: - Language servers now track their working directory more accurately. --------- Co-authored-by: João <joao@zed.dev>
This commit is contained in:
parent
2c2a3ef13d
commit
08b3c03241
29 changed files with 2151 additions and 943 deletions
|
@ -29,6 +29,7 @@ serde.workspace = true
|
|||
serde_json.workspace = true
|
||||
schemars.workspace = true
|
||||
smol.workspace = true
|
||||
text.workspace = true
|
||||
util.workspace = true
|
||||
release_channel.workspace = true
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ 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::{
|
||||
|
@ -21,12 +22,14 @@ use smol::{
|
|||
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
|
||||
process::Child,
|
||||
};
|
||||
use text::BufferId;
|
||||
|
||||
use std::{
|
||||
collections::BTreeSet,
|
||||
ffi::{OsStr, OsString},
|
||||
fmt,
|
||||
io::Write,
|
||||
ops::DerefMut,
|
||||
ops::{Deref, DerefMut},
|
||||
path::PathBuf,
|
||||
pin::Pin,
|
||||
sync::{
|
||||
|
@ -96,9 +99,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.
|
||||
|
@ -376,8 +379,6 @@ impl LanguageServer {
|
|||
Some(stderr),
|
||||
stderr_capture,
|
||||
Some(server),
|
||||
root_path,
|
||||
working_dir,
|
||||
code_action_kinds,
|
||||
binary,
|
||||
cx,
|
||||
|
@ -403,8 +404,6 @@ 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,
|
||||
|
@ -488,9 +487,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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -615,12 +614,11 @@ 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: Some(root_uri.clone()),
|
||||
root_uri: None,
|
||||
initialization_options: None,
|
||||
capabilities: ClientCapabilities {
|
||||
general: Some(GeneralClientCapabilities {
|
||||
|
@ -787,10 +785,7 @@ impl LanguageServer {
|
|||
}),
|
||||
},
|
||||
trace: None,
|
||||
workspace_folders: Some(vec![WorkspaceFolder {
|
||||
uri: root_uri,
|
||||
name: Default::default(),
|
||||
}]),
|
||||
workspace_folders: None,
|
||||
client_info: release_channel::ReleaseChannel::try_global(cx).map(|release_channel| {
|
||||
ClientInfo {
|
||||
name: release_channel.display_name().to_string(),
|
||||
|
@ -809,16 +804,10 @@ impl LanguageServer {
|
|||
/// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize)
|
||||
pub fn initialize(
|
||||
mut self,
|
||||
initialize_params: Option<InitializeParams>,
|
||||
params: 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 {
|
||||
|
@ -1070,16 +1059,10 @@ 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)
|
||||
|
@ -1207,6 +1190,129 @@ 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 {
|
||||
|
@ -1288,8 +1394,6 @@ 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(
|
||||
|
@ -1300,8 +1404,6 @@ impl FakeLanguageServer {
|
|||
None::<async_pipe::PipeReader>,
|
||||
Arc::new(Mutex::new(None)),
|
||||
None,
|
||||
root,
|
||||
root,
|
||||
None,
|
||||
binary.clone(),
|
||||
cx.clone(),
|
||||
|
@ -1319,8 +1421,6 @@ impl FakeLanguageServer {
|
|||
None::<async_pipe::PipeReader>,
|
||||
Arc::new(Mutex::new(None)),
|
||||
None,
|
||||
root,
|
||||
root,
|
||||
None,
|
||||
binary,
|
||||
cx.clone(),
|
||||
|
@ -1357,16 +1457,6 @@ 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"))]
|
||||
|
@ -1554,12 +1644,14 @@ mod tests {
|
|||
})
|
||||
.detach();
|
||||
|
||||
let initialize_params = None;
|
||||
let configuration = DidChangeConfigurationParams {
|
||||
settings: Default::default(),
|
||||
};
|
||||
let server = cx
|
||||
.update(|cx| server.initialize(initialize_params, configuration.into(), cx))
|
||||
.update(|cx| {
|
||||
let params = server.default_initialize_params(cx);
|
||||
let configuration = DidChangeConfigurationParams {
|
||||
settings: Default::default(),
|
||||
};
|
||||
server.initialize(params, configuration.into(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
server
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue