Extract an LspStore object from Project, to prepare for language support over SSH (#17041)

For ssh remoting lsps we'll need to have language server support
factored out of project.

Thus that begins

Release Notes:

- N/A

---------

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Mikayla <mikayla@zed.dev>
This commit is contained in:
Conrad Irwin 2024-08-30 15:36:38 -06:00 committed by GitHub
parent 7c57ffafbd
commit 75d4c7981e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 7252 additions and 6466 deletions

View file

@ -1,13 +1,20 @@
use std::{
cell::RefCell,
path::{Path, PathBuf},
sync::Arc,
sync::{atomic::AtomicUsize, Arc},
};
use anyhow::{anyhow, Context as _, Result};
use client::{Client, DevServerProjectId};
use collections::{HashMap, HashSet};
use fs::Fs;
use futures::{future::BoxFuture, SinkExt};
use gpui::{AppContext, AsyncAppContext, EntityId, EventEmitter, Model, ModelContext, WeakModel};
use futures::{
future::{BoxFuture, Shared},
FutureExt, SinkExt,
};
use gpui::{
AppContext, AsyncAppContext, EntityId, EventEmitter, Model, ModelContext, Task, WeakModel,
};
use postage::oneshot;
use rpc::{
proto::{self, AnyProtoClient},
@ -15,7 +22,6 @@ use rpc::{
};
use smol::{
channel::{Receiver, Sender},
future::FutureExt,
stream::StreamExt,
};
use text::ReplicaId;
@ -31,9 +37,16 @@ struct MatchingEntry {
}
pub struct WorktreeStore {
next_entry_id: Arc<AtomicUsize>,
upstream_client: Option<AnyProtoClient>,
dev_server_project_id: Option<DevServerProjectId>,
is_shared: bool,
worktrees: Vec<WorktreeHandle>,
worktrees_reordered: bool,
#[allow(clippy::type_complexity)]
loading_worktrees:
HashMap<Arc<Path>, Shared<Task<Result<Model<Worktree>, Arc<anyhow::Error>>>>>,
fs: Arc<dyn Fs>,
}
pub enum WorktreeStoreEvent {
@ -45,14 +58,35 @@ pub enum WorktreeStoreEvent {
impl EventEmitter<WorktreeStoreEvent> for WorktreeStore {}
impl WorktreeStore {
pub fn new(retain_worktrees: bool) -> Self {
pub fn init(client: &Arc<Client>) {
client.add_model_request_handler(WorktreeStore::handle_create_project_entry);
client.add_model_request_handler(WorktreeStore::handle_rename_project_entry);
client.add_model_request_handler(WorktreeStore::handle_copy_project_entry);
client.add_model_request_handler(WorktreeStore::handle_delete_project_entry);
client.add_model_request_handler(WorktreeStore::handle_expand_project_entry);
}
pub fn new(retain_worktrees: bool, fs: Arc<dyn Fs>) -> Self {
Self {
next_entry_id: Default::default(),
loading_worktrees: Default::default(),
upstream_client: None,
dev_server_project_id: None,
is_shared: retain_worktrees,
worktrees: Vec::new(),
worktrees_reordered: false,
fs,
}
}
pub fn set_upstream_client(&mut self, client: AnyProtoClient) {
self.upstream_client = Some(client);
}
pub fn set_dev_server_project_id(&mut self, id: DevServerProjectId) {
self.dev_server_project_id = Some(id);
}
/// Iterates through all worktrees, including ones that don't appear in the project panel
pub fn worktrees(&self) -> impl '_ + DoubleEndedIterator<Item = Model<Worktree>> {
self.worktrees
@ -83,6 +117,19 @@ impl WorktreeStore {
.find(|worktree| worktree.read(cx).contains_entry(entry_id))
}
pub fn find_worktree(
&self,
abs_path: &Path,
cx: &AppContext,
) -> Option<(Model<Worktree>, PathBuf)> {
for tree in self.worktrees() {
if let Ok(relative_path) = abs_path.strip_prefix(tree.read(cx).abs_path()) {
return Some((tree.clone(), relative_path.into()));
}
}
None
}
pub fn entry_for_id<'a>(
&'a self,
entry_id: ProjectEntryId,
@ -92,6 +139,157 @@ impl WorktreeStore {
.find_map(|worktree| worktree.read(cx).entry_for_id(entry_id))
}
pub fn entry_for_path(&self, path: &ProjectPath, cx: &AppContext) -> Option<Entry> {
self.worktree_for_id(path.worktree_id, cx)?
.read(cx)
.entry_for_path(&path.path)
.cloned()
}
pub fn create_worktree(
&mut self,
abs_path: impl AsRef<Path>,
visible: bool,
cx: &mut ModelContext<Self>,
) -> Task<Result<Model<Worktree>>> {
let path: Arc<Path> = abs_path.as_ref().into();
if !self.loading_worktrees.contains_key(&path) {
let task = if let Some(client) = self.upstream_client.clone() {
if let Some(dev_server_project_id) = self.dev_server_project_id {
self.create_dev_server_worktree(client, dev_server_project_id, abs_path, cx)
} else {
self.create_ssh_worktree(client, abs_path, visible, cx)
}
} else {
self.create_local_worktree(abs_path, visible, cx)
};
self.loading_worktrees.insert(path.clone(), task.shared());
}
let task = self.loading_worktrees.get(&path).unwrap().clone();
cx.background_executor().spawn(async move {
let result = match task.await {
Ok(worktree) => Ok(worktree),
Err(err) => Err(anyhow!("{}", err)),
};
result
})
}
fn create_ssh_worktree(
&mut self,
client: AnyProtoClient,
abs_path: impl AsRef<Path>,
visible: bool,
cx: &mut ModelContext<Self>,
) -> Task<Result<Model<Worktree>, Arc<anyhow::Error>>> {
let abs_path = abs_path.as_ref();
let root_name = abs_path.file_name().unwrap().to_string_lossy().to_string();
let path = abs_path.to_string_lossy().to_string();
cx.spawn(|this, mut cx| async move {
let response = client
.request(proto::AddWorktree { path: path.clone() })
.await?;
let worktree = cx.update(|cx| {
Worktree::remote(
0,
0,
proto::WorktreeMetadata {
id: response.worktree_id,
root_name,
visible,
abs_path: path,
},
client,
cx,
)
})?;
this.update(&mut cx, |this, cx| this.add(&worktree, cx))?;
Ok(worktree)
})
}
fn create_local_worktree(
&mut self,
abs_path: impl AsRef<Path>,
visible: bool,
cx: &mut ModelContext<Self>,
) -> Task<Result<Model<Worktree>, Arc<anyhow::Error>>> {
let fs = self.fs.clone();
let next_entry_id = self.next_entry_id.clone();
let path: Arc<Path> = abs_path.as_ref().into();
cx.spawn(move |this, mut cx| async move {
let worktree = Worktree::local(path.clone(), visible, fs, next_entry_id, &mut cx).await;
this.update(&mut cx, |project, _| {
project.loading_worktrees.remove(&path);
})?;
let worktree = worktree?;
this.update(&mut cx, |this, cx| this.add(&worktree, cx))?;
if visible {
cx.update(|cx| {
cx.add_recent_document(&path);
})
.log_err();
}
Ok(worktree)
})
}
fn create_dev_server_worktree(
&mut self,
client: AnyProtoClient,
dev_server_project_id: DevServerProjectId,
abs_path: impl AsRef<Path>,
cx: &mut ModelContext<Self>,
) -> Task<Result<Model<Worktree>, Arc<anyhow::Error>>> {
let path: Arc<Path> = abs_path.as_ref().into();
let mut paths: Vec<String> = self
.visible_worktrees(cx)
.map(|worktree| worktree.read(cx).abs_path().to_string_lossy().to_string())
.collect();
paths.push(path.to_string_lossy().to_string());
let request = client.request(proto::UpdateDevServerProject {
dev_server_project_id: dev_server_project_id.0,
paths,
});
let abs_path = abs_path.as_ref().to_path_buf();
cx.spawn(move |project, mut cx| async move {
let (tx, rx) = futures::channel::oneshot::channel();
let tx = RefCell::new(Some(tx));
let Some(project) = project.upgrade() else {
return Err(anyhow!("project dropped"))?;
};
let observer = cx.update(|cx| {
cx.observe(&project, move |project, cx| {
let abs_path = abs_path.clone();
project.update(cx, |project, cx| {
if let Some((worktree, _)) = project.find_worktree(&abs_path, cx) {
if let Some(tx) = tx.borrow_mut().take() {
tx.send(worktree).ok();
}
}
})
})
})?;
request.await?;
let worktree = rx.await.map_err(|e| anyhow!(e))?;
drop(observer);
project.update(&mut cx, |project, _| {
project.loading_worktrees.remove(&path);
})?;
Ok(worktree)
})
}
pub fn add(&mut self, worktree: &Model<Worktree>, cx: &mut ModelContext<Self>) {
let push_strong_handle = self.is_shared || worktree.read(cx).is_visible();
let handle = if push_strong_handle {