From 8cc6df573cb5926ce501eb58e3ca898e43142a33 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 17 Sep 2024 14:13:37 -0600 Subject: [PATCH] SshLspAdapterDelegate (#17965) Release Notes: - N/A --- crates/assistant/src/assistant_panel.rs | 20 +- crates/languages/src/rust.rs | 2 +- crates/project/src/lsp_store.rs | 333 ++++++++++++------- crates/proto/proto/zed.proto | 28 +- crates/proto/src/proto.rs | 11 +- crates/remote/src/ssh_session.rs | 19 +- crates/remote_server/src/headless_project.rs | 2 + crates/remote_server/src/main.rs | 1 - 8 files changed, 268 insertions(+), 148 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 6eaa86f4a7..5d06720fe0 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -54,7 +54,7 @@ use language_model::{ use language_model::{LanguageModelImage, LanguageModelToolUse}; use multi_buffer::MultiBufferRow; use picker::{Picker, PickerDelegate}; -use project::lsp_store::ProjectLspAdapterDelegate; +use project::lsp_store::LocalLspAdapterDelegate; use project::{Project, Worktree}; use search::{buffer_search::DivRegistrar, BufferSearchBar}; use serde::{Deserialize, Serialize}; @@ -5384,18 +5384,16 @@ fn make_lsp_adapter_delegate( let worktree = project .worktrees(cx) .next() - .ok_or_else(|| anyhow!("no worktrees when constructing ProjectLspAdapterDelegate"))?; - let fs = if project.is_local() { - Some(project.fs().clone()) - } else { - None - }; + .ok_or_else(|| anyhow!("no worktrees when constructing LocalLspAdapterDelegate"))?; let http_client = project.client().http_client().clone(); project.lsp_store().update(cx, |lsp_store, cx| { - Ok( - ProjectLspAdapterDelegate::new(lsp_store, &worktree, http_client, fs, None, cx) - as Arc, - ) + Ok(LocalLspAdapterDelegate::new( + lsp_store, + &worktree, + http_client, + project.fs().clone(), + cx, + ) as Arc) }) }) } diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index b55f350b9d..a32ffe50f5 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -77,7 +77,7 @@ impl LspAdapter for RustLspAdapter { { Ok(()) => (Some(path), Some(env), None), Err(err) => { - log::error!("failed to run rust-analyzer after detecting it in PATH: binary: {:?}: {:?}", path, err); + log::error!("failed to run rust-analyzer after detecting it in PATH: binary: {:?}: {}", path, err); (None, None, None) } } diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 2c718a42ab..daacf26c3a 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -2305,8 +2305,7 @@ impl LspStore { .read(cx) .worktree_for_id(*worktree_id, cx)?; let state = this.as_local()?.language_servers.get(server_id)?; - let delegate = - ProjectLspAdapterDelegate::for_local(this, &worktree, cx); + let delegate = LocalLspAdapterDelegate::for_local(this, &worktree, cx); match state { LanguageServerState::Starting(_) => None, LanguageServerState::Running { @@ -4368,7 +4367,7 @@ impl LspStore { let response = this .update(&mut cx, |this, cx| { let worktree = this.worktree_for_id(worktree_id, cx)?; - let delegate = ProjectLspAdapterDelegate::for_local(this, &worktree, cx); + let delegate = LocalLspAdapterDelegate::for_local(this, &worktree, cx); anyhow::Ok( cx.spawn(|_, _| async move { delegate.which(command.as_os_str()).await }), ) @@ -4389,7 +4388,7 @@ impl LspStore { let response = this .update(&mut cx, |this, cx| { let worktree = this.worktree_for_id(worktree_id, cx)?; - let delegate = ProjectLspAdapterDelegate::for_local(this, &worktree, cx); + let delegate = LocalLspAdapterDelegate::for_local(this, &worktree, cx); anyhow::Ok(cx.spawn(|_, _| async move { delegate.shell_env().await })) })?? .await; @@ -4398,6 +4397,52 @@ impl LspStore { env: response.into_iter().collect(), }) } + pub async fn handle_try_exec( + this: Model, + envelope: TypedEnvelope, + mut cx: AsyncAppContext, + ) -> Result { + let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); + let binary = envelope + .payload + .binary + .ok_or_else(|| anyhow!("missing binary"))?; + let binary = LanguageServerBinary { + path: PathBuf::from(binary.path), + env: None, + arguments: binary.arguments.into_iter().map(Into::into).collect(), + }; + this.update(&mut cx, |this, cx| { + let worktree = this.worktree_for_id(worktree_id, cx)?; + let delegate = LocalLspAdapterDelegate::for_local(this, &worktree, cx); + anyhow::Ok(cx.spawn(|_, _| async move { delegate.try_exec(binary).await })) + })?? + .await?; + + Ok(proto::Ack {}) + } + + pub async fn handle_read_text_file( + this: Model, + envelope: TypedEnvelope, + mut cx: AsyncAppContext, + ) -> Result { + let path = envelope + .payload + .path + .ok_or_else(|| anyhow!("missing path"))?; + let worktree_id = WorktreeId::from_proto(path.worktree_id); + let path = PathBuf::from(path.path); + let response = this + .update(&mut cx, |this, cx| { + let worktree = this.worktree_for_id(worktree_id, cx)?; + let delegate = LocalLspAdapterDelegate::for_local(this, &worktree, cx); + anyhow::Ok(cx.spawn(|_, _| async move { delegate.read_text_file(path).await })) + })?? + .await?; + + Ok(proto::ReadTextFileResponse { text: response }) + } async fn handle_apply_additional_edits_for_completion( this: Model, @@ -4535,9 +4580,12 @@ impl LspStore { ) { let ssh = self.as_ssh().unwrap(); - let delegate = - ProjectLspAdapterDelegate::for_ssh(self, worktree, ssh.upstream_client.clone(), cx) - as Arc; + let delegate = Arc::new(SshLspAdapterDelegate { + lsp_store: cx.handle().downgrade(), + worktree: worktree.read(cx).snapshot(), + upstream_client: ssh.upstream_client.clone(), + language_registry: self.languages.clone(), + }) as Arc; // TODO: We should use `adapter` here instead of reaching through the `CachedLspAdapter`. let lsp_adapter = adapter.adapter.clone(); @@ -4645,7 +4693,7 @@ impl LspStore { let local = self.as_local().unwrap(); let stderr_capture = Arc::new(Mutex::new(Some(String::new()))); - let lsp_adapter_delegate = ProjectLspAdapterDelegate::for_local(self, worktree_handle, cx); + let lsp_adapter_delegate = LocalLspAdapterDelegate::for_local(self, worktree_handle, cx); let project_environment = local.environment.update(cx, |environment, cx| { environment.get_environment(Some(worktree_id), Some(worktree_path.clone()), cx) }); @@ -6938,18 +6986,32 @@ impl LspAdapter for SshLspAdapter { None } } +pub fn language_server_settings<'a, 'b: 'a>( + delegate: &'a dyn LspAdapterDelegate, + language: &str, + cx: &'b AppContext, +) -> Option<&'a LspSettings> { + ProjectSettings::get( + Some(SettingsLocation { + worktree_id: delegate.worktree_id(), + path: delegate.worktree_root_path(), + }), + cx, + ) + .lsp + .get(language) +} -pub struct ProjectLspAdapterDelegate { +pub struct LocalLspAdapterDelegate { lsp_store: WeakModel, worktree: worktree::Snapshot, - fs: Option>, + fs: Arc, http_client: Arc, language_registry: Arc, load_shell_env_task: Shared>>>, - upstream_client: Option, } -impl ProjectLspAdapterDelegate { +impl LocalLspAdapterDelegate { fn for_local( lsp_store: &LspStore, worktree: &Model, @@ -6957,45 +7019,37 @@ impl ProjectLspAdapterDelegate { ) -> Arc { let local = lsp_store .as_local() - .expect("ProjectLspAdapterDelegate cannot be constructed on a remote"); + .expect("LocalLspAdapterDelegate cannot be constructed on a remote"); let http_client = local .http_client .clone() .unwrap_or_else(|| Arc::new(BlockedHttpClient)); - Self::new( - lsp_store, - worktree, - http_client, - Some(local.fs.clone()), - None, - cx, - ) + Self::new(lsp_store, worktree, http_client, local.fs.clone(), cx) } - fn for_ssh( - lsp_store: &LspStore, - worktree: &Model, - upstream_client: AnyProtoClient, - cx: &mut ModelContext, - ) -> Arc { - Self::new( - lsp_store, - worktree, - Arc::new(BlockedHttpClient), - None, - Some(upstream_client), - cx, - ) - } + // fn for_ssh( + // lsp_store: &LspStore, + // worktree: &Model, + // upstream_client: AnyProtoClient, + // cx: &mut ModelContext, + // ) -> Arc { + // Self::new( + // lsp_store, + // worktree, + // Arc::new(BlockedHttpClient), + // None, + // Some(upstream_client), + // cx, + // ) + // } pub fn new( lsp_store: &LspStore, worktree: &Model, http_client: Arc, - fs: Option>, - upstream_client: Option, + fs: Arc, cx: &mut ModelContext, ) -> Arc { let worktree_id = worktree.read(cx).id(); @@ -7015,52 +7069,14 @@ impl ProjectLspAdapterDelegate { worktree: worktree.read(cx).snapshot(), fs, http_client, - upstream_client, language_registry: lsp_store.languages.clone(), load_shell_env_task, }) } } -struct BlockedHttpClient; - -impl HttpClient for BlockedHttpClient { - fn send( - &self, - _req: Request, - ) -> BoxFuture<'static, Result, Error>> { - Box::pin(async { - Err(std::io::Error::new( - std::io::ErrorKind::PermissionDenied, - "ssh host blocked http connection", - ) - .into()) - }) - } - - fn proxy(&self) -> Option<&Uri> { - None - } -} - -pub fn language_server_settings<'a, 'b: 'a>( - delegate: &'a dyn LspAdapterDelegate, - language: &str, - cx: &'b AppContext, -) -> Option<&'a LspSettings> { - ProjectSettings::get( - Some(SettingsLocation { - worktree_id: delegate.worktree_id(), - path: delegate.worktree_root_path(), - }), - cx, - ) - .lsp - .get(language) -} - #[async_trait] -impl LspAdapterDelegate for ProjectLspAdapterDelegate { +impl LspAdapterDelegate for LocalLspAdapterDelegate { fn show_notification(&self, message: &str, cx: &mut AppContext) { self.lsp_store .update(cx, |_, cx| { @@ -7082,42 +7098,12 @@ impl LspAdapterDelegate for ProjectLspAdapterDelegate { } async fn shell_env(&self) -> HashMap { - if let Some(upstream_client) = &self.upstream_client { - use rpc::proto::SSH_PROJECT_ID; - - return upstream_client - .request(proto::ShellEnv { - project_id: SSH_PROJECT_ID, - worktree_id: self.worktree_id().to_proto(), - }) - .await - .map(|response| response.env.into_iter().collect()) - .unwrap_or_default(); - } - let task = self.load_shell_env_task.clone(); task.await.unwrap_or_default() } #[cfg(not(target_os = "windows"))] async fn which(&self, command: &OsStr) -> Option { - if let Some(upstream_client) = &self.upstream_client { - use rpc::proto::SSH_PROJECT_ID; - - return upstream_client - .request(proto::WhichCommand { - project_id: SSH_PROJECT_ID, - worktree_id: self.worktree_id().to_proto(), - command: command.to_string_lossy().to_string(), - }) - .await - .log_err() - .and_then(|response| response.path) - .map(PathBuf::from); - } - - self.fs.as_ref()?; - let worktree_abs_path = self.worktree.abs_path(); let shell_path = self.shell_env().await.get("PATH").cloned(); which::which_in(command, shell_path.as_ref(), worktree_abs_path).ok() @@ -7125,8 +7111,6 @@ impl LspAdapterDelegate for ProjectLspAdapterDelegate { #[cfg(target_os = "windows")] async fn which(&self, command: &OsStr) -> Option { - self.fs.as_ref()?; - // todo(windows) Getting the shell env variables in a current directory on Windows is more complicated than other platforms // there isn't a 'default shell' necessarily. The closest would be the default profile on the windows terminal // SEE: https://learn.microsoft.com/en-us/windows/terminal/customize-settings/startup @@ -7134,10 +7118,6 @@ impl LspAdapterDelegate for ProjectLspAdapterDelegate { } async fn try_exec(&self, command: LanguageServerBinary) -> Result<()> { - if self.fs.is_none() { - return Ok(()); - } - let working_dir = self.worktree_root_path(); let output = smol::process::Command::new(&command.path) .args(command.arguments) @@ -7170,12 +7150,127 @@ impl LspAdapterDelegate for ProjectLspAdapterDelegate { if self.worktree.entry_for_path(&path).is_none() { return Err(anyhow!("no such path {path:?}")); }; - if let Some(fs) = &self.fs { - let content = fs.load(&path).await?; - Ok(content) - } else { - return Err(anyhow!("cannot open {path:?} on ssh host (yet!)")); - } + self.fs.load(&path).await + } +} + +struct BlockedHttpClient; + +impl HttpClient for BlockedHttpClient { + fn send( + &self, + _req: Request, + ) -> BoxFuture<'static, Result, Error>> { + Box::pin(async { + Err(std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + "ssh host blocked http connection", + ) + .into()) + }) + } + + fn proxy(&self) -> Option<&Uri> { + None + } +} + +struct SshLspAdapterDelegate { + lsp_store: WeakModel, + worktree: worktree::Snapshot, + upstream_client: AnyProtoClient, + language_registry: Arc, +} + +#[async_trait] +impl LspAdapterDelegate for SshLspAdapterDelegate { + fn show_notification(&self, message: &str, cx: &mut AppContext) { + self.lsp_store + .update(cx, |_, cx| { + cx.emit(LspStoreEvent::Notification(message.to_owned())) + }) + .ok(); + } + + fn http_client(&self) -> Arc { + Arc::new(BlockedHttpClient) + } + + fn worktree_id(&self) -> WorktreeId { + self.worktree.id() + } + + fn worktree_root_path(&self) -> &Path { + self.worktree.abs_path().as_ref() + } + + async fn shell_env(&self) -> HashMap { + use rpc::proto::SSH_PROJECT_ID; + + self.upstream_client + .request(proto::ShellEnv { + project_id: SSH_PROJECT_ID, + worktree_id: self.worktree_id().to_proto(), + }) + .await + .map(|response| response.env.into_iter().collect()) + .unwrap_or_default() + } + + async fn which(&self, command: &OsStr) -> Option { + use rpc::proto::SSH_PROJECT_ID; + + self.upstream_client + .request(proto::WhichCommand { + project_id: SSH_PROJECT_ID, + worktree_id: self.worktree_id().to_proto(), + command: command.to_string_lossy().to_string(), + }) + .await + .log_err() + .and_then(|response| response.path) + .map(PathBuf::from) + } + + async fn try_exec(&self, command: LanguageServerBinary) -> Result<()> { + self.upstream_client + .request(proto::TryExec { + project_id: rpc::proto::SSH_PROJECT_ID, + worktree_id: self.worktree.id().to_proto(), + binary: Some(proto::LanguageServerCommand { + path: command.path.to_string_lossy().to_string(), + arguments: command + .arguments + .into_iter() + .map(|s| s.to_string_lossy().to_string()) + .collect(), + env: command.env.unwrap_or_default().into_iter().collect(), + }), + }) + .await?; + Ok(()) + } + + fn update_status( + &self, + server_name: LanguageServerName, + status: language::LanguageServerBinaryStatus, + ) { + self.language_registry + .update_lsp_status(server_name, status); + } + + async fn read_text_file(&self, path: PathBuf) -> Result { + self.upstream_client + .request(proto::ReadTextFile { + project_id: rpc::proto::SSH_PROJECT_ID, + path: Some(proto::ProjectPath { + worktree_id: self.worktree.id().to_proto(), + path: path.to_string_lossy().to_string(), + }), + }) + .await + .map(|r| r.text) } } diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 77942c8a94..a886b21855 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -289,7 +289,11 @@ message Envelope { WhichCommandResponse which_command_response = 249; ShellEnv shell_env = 250; - ShellEnvResponse shell_env_response = 251; // current max + ShellEnvResponse shell_env_response = 251; + + TryExec try_exec = 252; + ReadTextFile read_text_file = 253; + ReadTextFileResponse read_text_file_response = 254; // current max } reserved 158 to 161; @@ -2551,13 +2555,21 @@ message ShellEnvResponse { map env = 1; } -// message RestartLanguageServer { +message ReadTextFile { + uint64 project_id = 1; + ProjectPath path = 2; +} -// } -// message DestroyLanguageServer { +message ReadTextFileResponse { + string text = 1; +} -// } +message TryExec { + uint64 project_id = 1; + uint64 worktree_id = 2; + LanguageServerCommand binary = 3; +} -// message LspWorkspaceConfiguration { - -// } +message TryExecResponse { + string text = 1; +} diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index a1853ed4a3..b5a00d1670 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -370,6 +370,9 @@ messages!( (WhichCommandResponse, Foreground), (ShellEnv, Foreground), (ShellEnvResponse, Foreground), + (TryExec, Foreground), + (ReadTextFile, Foreground), + (ReadTextFileResponse, Foreground) ); request_messages!( @@ -495,7 +498,9 @@ request_messages!( (AddWorktree, AddWorktreeResponse), (CreateLanguageServer, Ack), (WhichCommand, WhichCommandResponse), - (ShellEnv, ShellEnvResponse) + (ShellEnv, ShellEnvResponse), + (ReadTextFile, ReadTextFileResponse), + (TryExec, Ack), ); entity_messages!( @@ -571,7 +576,9 @@ entity_messages!( UpdateUserSettings, CreateLanguageServer, WhichCommand, - ShellEnv + ShellEnv, + TryExec, + ReadTextFile ); entity_messages!( diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index 4762a785db..7556b38f3e 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -15,7 +15,7 @@ use gpui::{AppContext, AsyncAppContext, Model, SemanticVersion}; use parking_lot::Mutex; use rpc::{ proto::{self, build_typed_envelope, Envelope, EnvelopedMessage, PeerId, RequestMessage}, - EntityMessageSubscriber, ProtoClient, ProtoMessageHandlerSet, + EntityMessageSubscriber, ProtoClient, ProtoMessageHandlerSet, RpcError, }; use smol::{ fs, @@ -157,8 +157,9 @@ impl SshSession { let mut remote_server_child = socket .ssh_command(format!( - "RUST_LOG={} {:?} run", + "RUST_LOG={} RUST_BACKTRACE={} {:?} run", std::env::var("RUST_LOG").unwrap_or_default(), + std::env::var("RUST_BACKTRACE").unwrap_or_default(), remote_binary_path, )) .spawn() @@ -349,7 +350,7 @@ impl SshSession { } Err(error) => { log::error!( - "error handling message. type:{type_name}, error:{error:?}", + "error handling message. type:{type_name}, error:{error}", ); } } @@ -371,7 +372,7 @@ impl SshSession { payload: T, ) -> impl 'static + Future> { log::debug!("ssh request start. name:{}", T::NAME); - let response = self.request_dynamic(payload.into_envelope(0, None, None), ""); + let response = self.request_dynamic(payload.into_envelope(0, None, None), T::NAME); async move { let response = response.await?; log::debug!("ssh request finish. name:{}", T::NAME); @@ -388,7 +389,7 @@ impl SshSession { pub fn request_dynamic( &self, mut envelope: proto::Envelope, - _request_type: &'static str, + type_name: &'static str, ) -> impl 'static + Future> { envelope.id = self.next_message_id.fetch_add(1, SeqCst); let (tx, rx) = oneshot::channel(); @@ -396,7 +397,13 @@ impl SshSession { response_channels_lock.insert(MessageId(envelope.id), tx); drop(response_channels_lock); self.outgoing_tx.unbounded_send(envelope).ok(); - async move { Ok(rx.await.context("connection lost")?.0) } + async move { + let response = rx.await.context("connection lost")?.0; + if let Some(proto::envelope::Payload::Error(error)) = &response.payload { + return Err(RpcError::from_proto(error, type_name)); + } + Ok(response) + } } pub fn send_dynamic(&self, mut envelope: proto::Envelope) -> Result<()> { diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 58f5cb0c20..35d6630c1e 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -107,6 +107,8 @@ impl HeadlessProject { client.add_model_request_handler(LspStore::handle_create_language_server); client.add_model_request_handler(LspStore::handle_which_command); client.add_model_request_handler(LspStore::handle_shell_env); + client.add_model_request_handler(LspStore::handle_try_exec); + client.add_model_request_handler(LspStore::handle_read_text_file); BufferStore::init(&client); WorktreeStore::init(&client); diff --git a/crates/remote_server/src/main.rs b/crates/remote_server/src/main.rs index 696022a456..908a0a89b6 100644 --- a/crates/remote_server/src/main.rs +++ b/crates/remote_server/src/main.rs @@ -24,7 +24,6 @@ fn main() { #[cfg(not(windows))] fn main() { - env::set_var("RUST_BACKTRACE", "1"); env_logger::builder() .format(|buf, record| { serde_json::to_writer(&mut *buf, &LogRecord::new(record))?;