diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 7eebc97b1d..a0d4cbcf81 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -5349,7 +5349,7 @@ fn make_lsp_adapter_delegate( 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, cx) + ProjectLspAdapterDelegate::new(lsp_store, &worktree, http_client, fs, None, cx) as Arc, ) }) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 6424da8a54..cd39490d0b 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -1651,6 +1651,14 @@ impl LspAdapter for FakeLspAdapter { LanguageServerName(self.name.into()) } + async fn check_if_user_installed( + &self, + _: &dyn LspAdapterDelegate, + _: &AsyncAppContext, + ) -> Option { + Some(self.language_server_binary.clone()) + } + fn get_language_server_command<'a>( self: Arc, _: Arc, diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index b218ac5804..3b6b9ebb0a 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -442,6 +442,17 @@ impl LspStore { } } + fn worktree_for_id( + &self, + worktree_id: WorktreeId, + cx: &ModelContext, + ) -> Result> { + self.worktree_store + .read(cx) + .worktree_for_id(worktree_id, cx) + .ok_or_else(|| anyhow!("worktree not found")) + } + fn on_buffer_store_event( &mut self, _: Model, @@ -4287,6 +4298,7 @@ impl LspStore { .ok_or_else(|| anyhow!("missing language"))?; let language_name = LanguageName::from_proto(language.name); let matcher: LanguageMatcher = serde_json::from_str(&language.matcher)?; + this.update(&mut cx, |this, cx| { this.languages .register_language(language_name.clone(), None, matcher.clone(), { @@ -4334,6 +4346,47 @@ impl LspStore { Ok(proto::Ack {}) } + pub async fn handle_which_command( + this: Model, + envelope: TypedEnvelope, + mut cx: AsyncAppContext, + ) -> Result { + let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); + let command = PathBuf::from(envelope.payload.command); + 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); + anyhow::Ok( + cx.spawn(|_, _| async move { delegate.which(command.as_os_str()).await }), + ) + })?? + .await; + + Ok(proto::WhichCommandResponse { + path: response.map(|path| path.to_string_lossy().to_string()), + }) + } + + pub async fn handle_shell_env( + this: Model, + envelope: TypedEnvelope, + mut cx: AsyncAppContext, + ) -> Result { + let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); + 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); + anyhow::Ok(cx.spawn(|_, _| async move { delegate.shell_env().await })) + })?? + .await; + + Ok(proto::ShellEnvResponse { + env: response.into_iter().collect(), + }) + } + async fn handle_apply_additional_edits_for_completion( this: Model, envelope: TypedEnvelope, @@ -4478,39 +4531,34 @@ impl LspStore { ) { let ssh = self.as_ssh().unwrap(); - let configured_binary = ProjectSettings::get( - Some(worktree.update(cx, |worktree, cx| worktree.settings_location(cx))), - cx, - ) - .lsp - .get(&adapter.name()) - .and_then(|c| c.binary.as_ref()) - .and_then(|config| { - if let Some(path) = &config.path { - Some((path.clone(), config.arguments.clone().unwrap_or_default())) - } else { - None - } - }); let delegate = - ProjectLspAdapterDelegate::for_ssh(self, worktree, cx) as Arc; + ProjectLspAdapterDelegate::for_ssh(self, worktree, ssh.upstream_client.clone(), cx) + as Arc; + + // TODO: We should use `adapter` here instead of reaching through the `CachedLspAdapter`. + let lsp_adapter = adapter.adapter.clone(); + let project_id = self.project_id; let worktree_id = worktree.read(cx).id().to_proto(); let upstream_client = ssh.upstream_client.clone(); let name = adapter.name().to_string(); - let Some((path, arguments)) = configured_binary else { - cx.emit(LspStoreEvent::Notification(format!( - "ssh-remoting currently requires manually configuring {} in your settings", - adapter.name() - ))); - return; - }; + let Some(available_language) = self.languages.available_language_for_name(&language) else { log::error!("failed to find available language {language}"); return; }; - let task = cx.spawn(|_, _| async move { - let delegate = delegate; + + let task = cx.spawn(|_, cx| async move { + let user_binary_task = lsp_adapter.check_if_user_installed(delegate.as_ref(), &cx); + let binary = match user_binary_task.await { + Some(binary) => binary, + None => { + return Err(anyhow!( + "Downloading language server for ssh host is not supported yet" + )) + } + }; + let name = adapter.name().to_string(); let code_action_kinds = adapter .adapter @@ -4523,12 +4571,22 @@ impl LspStore { .map(|options| serde_json::to_string(&options)) .transpose()?; + let language_server_command = proto::LanguageServerCommand { + path: binary.path.to_string_lossy().to_string(), + arguments: binary + .arguments + .iter() + .map(|args| args.to_string_lossy().to_string()) + .collect(), + env: binary.env.unwrap_or_default().into_iter().collect(), + }; + upstream_client .request(proto::CreateLanguageServer { project_id, worktree_id, name, - binary: Some(proto::LanguageServerCommand { path, arguments }), + binary: Some(language_server_command), initialization_options, code_action_kinds, language: Some(proto::AvailableLanguage { @@ -6890,6 +6948,7 @@ pub struct ProjectLspAdapterDelegate { http_client: Arc, language_registry: Arc, load_shell_env_task: Shared>>>, + upstream_client: Option, } impl ProjectLspAdapterDelegate { @@ -6907,15 +6966,30 @@ impl ProjectLspAdapterDelegate { .clone() .unwrap_or_else(|| Arc::new(BlockedHttpClient)); - Self::new(lsp_store, worktree, http_client, Some(local.fs.clone()), cx) + Self::new( + lsp_store, + worktree, + http_client, + Some(local.fs.clone()), + None, + 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, cx) + Self::new( + lsp_store, + worktree, + Arc::new(BlockedHttpClient), + None, + Some(upstream_client), + cx, + ) } pub fn new( @@ -6923,6 +6997,7 @@ impl ProjectLspAdapterDelegate { worktree: &Model, http_client: Arc, fs: Option>, + upstream_client: Option, cx: &mut ModelContext, ) -> Arc { let worktree_id = worktree.read(cx).id(); @@ -6942,6 +7017,7 @@ impl ProjectLspAdapterDelegate { worktree: worktree.read(cx).snapshot(), fs, http_client, + upstream_client, language_registry: lsp_store.languages.clone(), load_shell_env_task, }) @@ -6991,13 +7067,42 @@ 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() diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index b24d939965..e5d767fffb 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -283,7 +283,13 @@ message Envelope { CloseBuffer close_buffer = 245; UpdateUserSettings update_user_settings = 246; - CreateLanguageServer create_language_server = 247; // current max + CreateLanguageServer create_language_server = 247; + + WhichCommand which_command = 248; + WhichCommandResponse which_command_response = 249; + + ShellEnv shell_env = 250; + ShellEnvResponse shell_env_response = 251; // current max } reserved 158 to 161; @@ -2503,6 +2509,7 @@ message UpdateUserSettings { message LanguageServerCommand { string path = 1; repeated string arguments = 2; + map env = 3; } message AvailableLanguage { @@ -2522,6 +2529,25 @@ message CreateLanguageServer { AvailableLanguage language = 7; } +message WhichCommand { + uint64 project_id = 1; + uint64 worktree_id = 2; + string command = 3; +} + +message WhichCommandResponse { + optional string path = 1; +} + +message ShellEnv { + uint64 project_id = 1; + uint64 worktree_id = 2; +} + +message ShellEnvResponse { + map env = 1; +} + // message RestartLanguageServer { // } diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 44cb91db10..7af66a6a6b 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -367,7 +367,11 @@ messages!( (FindSearchCandidatesResponse, Background), (CloseBuffer, Foreground), (UpdateUserSettings, Foreground), - (CreateLanguageServer, Foreground) + (CreateLanguageServer, Foreground), + (WhichCommand, Foreground), + (WhichCommandResponse, Foreground), + (ShellEnv, Foreground), + (ShellEnvResponse, Foreground), ); request_messages!( @@ -491,7 +495,9 @@ request_messages!( (SynchronizeContexts, SynchronizeContextsResponse), (LspExtSwitchSourceHeader, LspExtSwitchSourceHeaderResponse), (AddWorktree, AddWorktreeResponse), - (CreateLanguageServer, Ack) + (CreateLanguageServer, Ack), + (WhichCommand, WhichCommandResponse), + (ShellEnv, ShellEnvResponse) ); entity_messages!( @@ -565,7 +571,9 @@ entity_messages!( SynchronizeContexts, LspExtSwitchSourceHeader, UpdateUserSettings, - CreateLanguageServer + CreateLanguageServer, + WhichCommand, + ShellEnv ); entity_messages!( diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index ca5fe06e13..e654e2a190 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -89,6 +89,8 @@ impl HeadlessProject { client.add_model_message_handler(BufferStore::handle_close_buffer); 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); BufferStore::init(&client); WorktreeStore::init(&client);