ssh: Lookup language servers in env on SSH host (#17658)
Release Notes: - ssh remoting: Lookup language server binaries in environment on SSH host --------- Co-authored-by: Bennet <bennet@zed.dev>
This commit is contained in:
parent
19463b59e2
commit
48a16f9e70
6 changed files with 181 additions and 32 deletions
|
@ -5349,7 +5349,7 @@ fn make_lsp_adapter_delegate(
|
||||||
let http_client = project.client().http_client().clone();
|
let http_client = project.client().http_client().clone();
|
||||||
project.lsp_store().update(cx, |lsp_store, cx| {
|
project.lsp_store().update(cx, |lsp_store, cx| {
|
||||||
Ok(
|
Ok(
|
||||||
ProjectLspAdapterDelegate::new(lsp_store, &worktree, http_client, fs, cx)
|
ProjectLspAdapterDelegate::new(lsp_store, &worktree, http_client, fs, None, cx)
|
||||||
as Arc<dyn LspAdapterDelegate>,
|
as Arc<dyn LspAdapterDelegate>,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -1651,6 +1651,14 @@ impl LspAdapter for FakeLspAdapter {
|
||||||
LanguageServerName(self.name.into())
|
LanguageServerName(self.name.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn check_if_user_installed(
|
||||||
|
&self,
|
||||||
|
_: &dyn LspAdapterDelegate,
|
||||||
|
_: &AsyncAppContext,
|
||||||
|
) -> Option<LanguageServerBinary> {
|
||||||
|
Some(self.language_server_binary.clone())
|
||||||
|
}
|
||||||
|
|
||||||
fn get_language_server_command<'a>(
|
fn get_language_server_command<'a>(
|
||||||
self: Arc<Self>,
|
self: Arc<Self>,
|
||||||
_: Arc<Path>,
|
_: Arc<Path>,
|
||||||
|
|
|
@ -442,6 +442,17 @@ impl LspStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn worktree_for_id(
|
||||||
|
&self,
|
||||||
|
worktree_id: WorktreeId,
|
||||||
|
cx: &ModelContext<Self>,
|
||||||
|
) -> Result<Model<Worktree>> {
|
||||||
|
self.worktree_store
|
||||||
|
.read(cx)
|
||||||
|
.worktree_for_id(worktree_id, cx)
|
||||||
|
.ok_or_else(|| anyhow!("worktree not found"))
|
||||||
|
}
|
||||||
|
|
||||||
fn on_buffer_store_event(
|
fn on_buffer_store_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: Model<BufferStore>,
|
_: Model<BufferStore>,
|
||||||
|
@ -4287,6 +4298,7 @@ impl LspStore {
|
||||||
.ok_or_else(|| anyhow!("missing language"))?;
|
.ok_or_else(|| anyhow!("missing language"))?;
|
||||||
let language_name = LanguageName::from_proto(language.name);
|
let language_name = LanguageName::from_proto(language.name);
|
||||||
let matcher: LanguageMatcher = serde_json::from_str(&language.matcher)?;
|
let matcher: LanguageMatcher = serde_json::from_str(&language.matcher)?;
|
||||||
|
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
this.languages
|
this.languages
|
||||||
.register_language(language_name.clone(), None, matcher.clone(), {
|
.register_language(language_name.clone(), None, matcher.clone(), {
|
||||||
|
@ -4334,6 +4346,47 @@ impl LspStore {
|
||||||
Ok(proto::Ack {})
|
Ok(proto::Ack {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn handle_which_command(
|
||||||
|
this: Model<Self>,
|
||||||
|
envelope: TypedEnvelope<proto::WhichCommand>,
|
||||||
|
mut cx: AsyncAppContext,
|
||||||
|
) -> Result<proto::WhichCommandResponse> {
|
||||||
|
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<Self>,
|
||||||
|
envelope: TypedEnvelope<proto::ShellEnv>,
|
||||||
|
mut cx: AsyncAppContext,
|
||||||
|
) -> Result<proto::ShellEnvResponse> {
|
||||||
|
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(
|
async fn handle_apply_additional_edits_for_completion(
|
||||||
this: Model<Self>,
|
this: Model<Self>,
|
||||||
envelope: TypedEnvelope<proto::ApplyCompletionAdditionalEdits>,
|
envelope: TypedEnvelope<proto::ApplyCompletionAdditionalEdits>,
|
||||||
|
@ -4478,39 +4531,34 @@ impl LspStore {
|
||||||
) {
|
) {
|
||||||
let ssh = self.as_ssh().unwrap();
|
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 =
|
let delegate =
|
||||||
ProjectLspAdapterDelegate::for_ssh(self, worktree, cx) as Arc<dyn LspAdapterDelegate>;
|
ProjectLspAdapterDelegate::for_ssh(self, worktree, ssh.upstream_client.clone(), cx)
|
||||||
|
as Arc<dyn LspAdapterDelegate>;
|
||||||
|
|
||||||
|
// 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 project_id = self.project_id;
|
||||||
let worktree_id = worktree.read(cx).id().to_proto();
|
let worktree_id = worktree.read(cx).id().to_proto();
|
||||||
let upstream_client = ssh.upstream_client.clone();
|
let upstream_client = ssh.upstream_client.clone();
|
||||||
let name = adapter.name().to_string();
|
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 {
|
let Some(available_language) = self.languages.available_language_for_name(&language) else {
|
||||||
log::error!("failed to find available language {language}");
|
log::error!("failed to find available language {language}");
|
||||||
return;
|
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 name = adapter.name().to_string();
|
||||||
let code_action_kinds = adapter
|
let code_action_kinds = adapter
|
||||||
.adapter
|
.adapter
|
||||||
|
@ -4523,12 +4571,22 @@ impl LspStore {
|
||||||
.map(|options| serde_json::to_string(&options))
|
.map(|options| serde_json::to_string(&options))
|
||||||
.transpose()?;
|
.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
|
upstream_client
|
||||||
.request(proto::CreateLanguageServer {
|
.request(proto::CreateLanguageServer {
|
||||||
project_id,
|
project_id,
|
||||||
worktree_id,
|
worktree_id,
|
||||||
name,
|
name,
|
||||||
binary: Some(proto::LanguageServerCommand { path, arguments }),
|
binary: Some(language_server_command),
|
||||||
initialization_options,
|
initialization_options,
|
||||||
code_action_kinds,
|
code_action_kinds,
|
||||||
language: Some(proto::AvailableLanguage {
|
language: Some(proto::AvailableLanguage {
|
||||||
|
@ -6890,6 +6948,7 @@ pub struct ProjectLspAdapterDelegate {
|
||||||
http_client: Arc<dyn HttpClient>,
|
http_client: Arc<dyn HttpClient>,
|
||||||
language_registry: Arc<LanguageRegistry>,
|
language_registry: Arc<LanguageRegistry>,
|
||||||
load_shell_env_task: Shared<Task<Option<HashMap<String, String>>>>,
|
load_shell_env_task: Shared<Task<Option<HashMap<String, String>>>>,
|
||||||
|
upstream_client: Option<AnyProtoClient>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProjectLspAdapterDelegate {
|
impl ProjectLspAdapterDelegate {
|
||||||
|
@ -6907,15 +6966,30 @@ impl ProjectLspAdapterDelegate {
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| Arc::new(BlockedHttpClient));
|
.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(
|
fn for_ssh(
|
||||||
lsp_store: &LspStore,
|
lsp_store: &LspStore,
|
||||||
worktree: &Model<Worktree>,
|
worktree: &Model<Worktree>,
|
||||||
|
upstream_client: AnyProtoClient,
|
||||||
cx: &mut ModelContext<LspStore>,
|
cx: &mut ModelContext<LspStore>,
|
||||||
) -> Arc<Self> {
|
) -> Arc<Self> {
|
||||||
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(
|
pub fn new(
|
||||||
|
@ -6923,6 +6997,7 @@ impl ProjectLspAdapterDelegate {
|
||||||
worktree: &Model<Worktree>,
|
worktree: &Model<Worktree>,
|
||||||
http_client: Arc<dyn HttpClient>,
|
http_client: Arc<dyn HttpClient>,
|
||||||
fs: Option<Arc<dyn Fs>>,
|
fs: Option<Arc<dyn Fs>>,
|
||||||
|
upstream_client: Option<AnyProtoClient>,
|
||||||
cx: &mut ModelContext<LspStore>,
|
cx: &mut ModelContext<LspStore>,
|
||||||
) -> Arc<Self> {
|
) -> Arc<Self> {
|
||||||
let worktree_id = worktree.read(cx).id();
|
let worktree_id = worktree.read(cx).id();
|
||||||
|
@ -6942,6 +7017,7 @@ impl ProjectLspAdapterDelegate {
|
||||||
worktree: worktree.read(cx).snapshot(),
|
worktree: worktree.read(cx).snapshot(),
|
||||||
fs,
|
fs,
|
||||||
http_client,
|
http_client,
|
||||||
|
upstream_client,
|
||||||
language_registry: lsp_store.languages.clone(),
|
language_registry: lsp_store.languages.clone(),
|
||||||
load_shell_env_task,
|
load_shell_env_task,
|
||||||
})
|
})
|
||||||
|
@ -6991,13 +7067,42 @@ impl LspAdapterDelegate for ProjectLspAdapterDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn shell_env(&self) -> HashMap<String, String> {
|
async fn shell_env(&self) -> HashMap<String, String> {
|
||||||
|
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();
|
let task = self.load_shell_env_task.clone();
|
||||||
task.await.unwrap_or_default()
|
task.await.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
async fn which(&self, command: &OsStr) -> Option<PathBuf> {
|
async fn which(&self, command: &OsStr) -> Option<PathBuf> {
|
||||||
|
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()?;
|
self.fs.as_ref()?;
|
||||||
|
|
||||||
let worktree_abs_path = self.worktree.abs_path();
|
let worktree_abs_path = self.worktree.abs_path();
|
||||||
let shell_path = self.shell_env().await.get("PATH").cloned();
|
let shell_path = self.shell_env().await.get("PATH").cloned();
|
||||||
which::which_in(command, shell_path.as_ref(), worktree_abs_path).ok()
|
which::which_in(command, shell_path.as_ref(), worktree_abs_path).ok()
|
||||||
|
|
|
@ -283,7 +283,13 @@ message Envelope {
|
||||||
CloseBuffer close_buffer = 245;
|
CloseBuffer close_buffer = 245;
|
||||||
UpdateUserSettings update_user_settings = 246;
|
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;
|
reserved 158 to 161;
|
||||||
|
@ -2503,6 +2509,7 @@ message UpdateUserSettings {
|
||||||
message LanguageServerCommand {
|
message LanguageServerCommand {
|
||||||
string path = 1;
|
string path = 1;
|
||||||
repeated string arguments = 2;
|
repeated string arguments = 2;
|
||||||
|
map<string, string> env = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message AvailableLanguage {
|
message AvailableLanguage {
|
||||||
|
@ -2522,6 +2529,25 @@ message CreateLanguageServer {
|
||||||
AvailableLanguage language = 7;
|
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<string, string> env = 1;
|
||||||
|
}
|
||||||
|
|
||||||
// message RestartLanguageServer {
|
// message RestartLanguageServer {
|
||||||
|
|
||||||
// }
|
// }
|
||||||
|
|
|
@ -367,7 +367,11 @@ messages!(
|
||||||
(FindSearchCandidatesResponse, Background),
|
(FindSearchCandidatesResponse, Background),
|
||||||
(CloseBuffer, Foreground),
|
(CloseBuffer, Foreground),
|
||||||
(UpdateUserSettings, Foreground),
|
(UpdateUserSettings, Foreground),
|
||||||
(CreateLanguageServer, Foreground)
|
(CreateLanguageServer, Foreground),
|
||||||
|
(WhichCommand, Foreground),
|
||||||
|
(WhichCommandResponse, Foreground),
|
||||||
|
(ShellEnv, Foreground),
|
||||||
|
(ShellEnvResponse, Foreground),
|
||||||
);
|
);
|
||||||
|
|
||||||
request_messages!(
|
request_messages!(
|
||||||
|
@ -491,7 +495,9 @@ request_messages!(
|
||||||
(SynchronizeContexts, SynchronizeContextsResponse),
|
(SynchronizeContexts, SynchronizeContextsResponse),
|
||||||
(LspExtSwitchSourceHeader, LspExtSwitchSourceHeaderResponse),
|
(LspExtSwitchSourceHeader, LspExtSwitchSourceHeaderResponse),
|
||||||
(AddWorktree, AddWorktreeResponse),
|
(AddWorktree, AddWorktreeResponse),
|
||||||
(CreateLanguageServer, Ack)
|
(CreateLanguageServer, Ack),
|
||||||
|
(WhichCommand, WhichCommandResponse),
|
||||||
|
(ShellEnv, ShellEnvResponse)
|
||||||
);
|
);
|
||||||
|
|
||||||
entity_messages!(
|
entity_messages!(
|
||||||
|
@ -565,7 +571,9 @@ entity_messages!(
|
||||||
SynchronizeContexts,
|
SynchronizeContexts,
|
||||||
LspExtSwitchSourceHeader,
|
LspExtSwitchSourceHeader,
|
||||||
UpdateUserSettings,
|
UpdateUserSettings,
|
||||||
CreateLanguageServer
|
CreateLanguageServer,
|
||||||
|
WhichCommand,
|
||||||
|
ShellEnv
|
||||||
);
|
);
|
||||||
|
|
||||||
entity_messages!(
|
entity_messages!(
|
||||||
|
|
|
@ -89,6 +89,8 @@ impl HeadlessProject {
|
||||||
client.add_model_message_handler(BufferStore::handle_close_buffer);
|
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_create_language_server);
|
||||||
|
client.add_model_request_handler(LspStore::handle_which_command);
|
||||||
|
client.add_model_request_handler(LspStore::handle_shell_env);
|
||||||
|
|
||||||
BufferStore::init(&client);
|
BufferStore::init(&client);
|
||||||
WorktreeStore::init(&client);
|
WorktreeStore::init(&client);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue