ssh lsp completions (#17665)

Release Notes:

* ssh-remoting: Fixed shell environment loading for remote shells.
This commit is contained in:
Conrad Irwin 2024-09-16 12:22:39 -06:00 committed by GitHub
parent dea85099a2
commit ca2cce79ed
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 74 additions and 78 deletions

View file

@ -12,7 +12,7 @@ use collections::{hash_map, HashMap, HashSet};
use futures::{ use futures::{
channel::{mpsc, oneshot}, channel::{mpsc, oneshot},
future::Shared, future::Shared,
Future, FutureExt as _, Future,
}; };
use globset::GlobSet; use globset::GlobSet;
use gpui::{AppContext, BackgroundExecutor, Task}; use gpui::{AppContext, BackgroundExecutor, Task};
@ -79,7 +79,6 @@ impl<'a> From<&'a str> for LanguageName {
pub struct LanguageRegistry { pub struct LanguageRegistry {
state: RwLock<LanguageRegistryState>, state: RwLock<LanguageRegistryState>,
language_server_download_dir: Option<Arc<Path>>, language_server_download_dir: Option<Arc<Path>>,
login_shell_env_loaded: Shared<Task<()>>,
executor: BackgroundExecutor, executor: BackgroundExecutor,
lsp_binary_status_tx: LspBinaryStatusSender, lsp_binary_status_tx: LspBinaryStatusSender,
} }
@ -204,7 +203,7 @@ struct LspBinaryStatusSender {
} }
impl LanguageRegistry { impl LanguageRegistry {
pub fn new(login_shell_env_loaded: Task<()>, executor: BackgroundExecutor) -> Self { pub fn new(executor: BackgroundExecutor) -> Self {
let this = Self { let this = Self {
state: RwLock::new(LanguageRegistryState { state: RwLock::new(LanguageRegistryState {
next_language_server_id: 0, next_language_server_id: 0,
@ -224,7 +223,6 @@ impl LanguageRegistry {
fake_server_txs: Default::default(), fake_server_txs: Default::default(),
}), }),
language_server_download_dir: None, language_server_download_dir: None,
login_shell_env_loaded: login_shell_env_loaded.shared(),
lsp_binary_status_tx: Default::default(), lsp_binary_status_tx: Default::default(),
executor, executor,
}; };
@ -234,7 +232,7 @@ impl LanguageRegistry {
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
pub fn test(executor: BackgroundExecutor) -> Self { pub fn test(executor: BackgroundExecutor) -> Self {
let mut this = Self::new(Task::ready(()), executor); let mut this = Self::new(executor);
this.language_server_download_dir = Some(Path::new("/the-download-dir").into()); this.language_server_download_dir = Some(Path::new("/the-download-dir").into());
this this
} }
@ -828,7 +826,7 @@ impl LanguageRegistry {
adapter: Arc<CachedLspAdapter>, adapter: Arc<CachedLspAdapter>,
root_path: Arc<Path>, root_path: Arc<Path>,
delegate: Arc<dyn LspAdapterDelegate>, delegate: Arc<dyn LspAdapterDelegate>,
cli_environment: Option<HashMap<String, String>>, cli_environment: Shared<Task<Option<HashMap<String, String>>>>,
cx: &mut AppContext, cx: &mut AppContext,
) -> Option<PendingLanguageServer> { ) -> Option<PendingLanguageServer> {
let server_id = self.state.write().next_language_server_id(); let server_id = self.state.write().next_language_server_id();
@ -844,15 +842,12 @@ impl LanguageRegistry {
.log_err()?; .log_err()?;
let container_dir: Arc<Path> = Arc::from(download_dir.join(adapter.name.0.as_ref())); let container_dir: Arc<Path> = Arc::from(download_dir.join(adapter.name.0.as_ref()));
let root_path = root_path.clone(); let root_path = root_path.clone();
let login_shell_env_loaded = self.login_shell_env_loaded.clone();
let this = Arc::downgrade(self); let this = Arc::downgrade(self);
let task = cx.spawn({ let task = cx.spawn({
let container_dir = container_dir.clone(); let container_dir = container_dir.clone();
move |mut cx| async move { move |mut cx| async move {
// If we want to install a binary globally, we need to wait for let cli_environment = cli_environment.await;
// the login shell to be set on our process.
login_shell_env_loaded.await;
let binary_result = adapter let binary_result = adapter
.clone() .clone()

View file

@ -1,5 +1,5 @@
use assets::Assets; use assets::Assets;
use gpui::{prelude::*, rgb, App, KeyBinding, StyleRefinement, Task, View, WindowOptions}; use gpui::{prelude::*, rgb, App, KeyBinding, StyleRefinement, View, WindowOptions};
use language::{language_settings::AllLanguageSettings, LanguageRegistry}; use language::{language_settings::AllLanguageSettings, LanguageRegistry};
use markdown::{Markdown, MarkdownStyle}; use markdown::{Markdown, MarkdownStyle};
use node_runtime::FakeNodeRuntime; use node_runtime::FakeNodeRuntime;
@ -105,8 +105,7 @@ pub fn main() {
let node_runtime = FakeNodeRuntime::new(); let node_runtime = FakeNodeRuntime::new();
theme::init(LoadThemes::JustBase, cx); theme::init(LoadThemes::JustBase, cx);
let language_registry = let language_registry = LanguageRegistry::new(cx.background_executor().clone());
LanguageRegistry::new(Task::ready(()), cx.background_executor().clone());
language_registry.set_theme(cx.theme().clone()); language_registry.set_theme(cx.theme().clone());
let language_registry = Arc::new(language_registry); let language_registry = Arc::new(language_registry);
languages::init(language_registry.clone(), node_runtime, cx); languages::init(language_registry.clone(), node_runtime, cx);

View file

@ -29,10 +29,7 @@ pub fn main() {
cx.bind_keys([KeyBinding::new("cmd-c", markdown::Copy, None)]); cx.bind_keys([KeyBinding::new("cmd-c", markdown::Copy, None)]);
let node_runtime = FakeNodeRuntime::new(); let node_runtime = FakeNodeRuntime::new();
let language_registry = Arc::new(LanguageRegistry::new( let language_registry = Arc::new(LanguageRegistry::new(cx.background_executor().clone()));
Task::ready(()),
cx.background_executor().clone(),
));
languages::init(language_registry.clone(), node_runtime, cx); languages::init(language_registry.clone(), node_runtime, cx);
theme::init(LoadThemes::JustBase, cx); theme::init(LoadThemes::JustBase, cx);
Assets.load_fonts(cx).unwrap(); Assets.load_fonts(cx).unwrap();

View file

@ -1,10 +1,7 @@
use anyhow::{anyhow, Context as _, Result}; use anyhow::Result;
use futures::{future::Shared, FutureExt}; use futures::{future::Shared, FutureExt};
use std::{ use std::{path::Path, sync::Arc};
path::{Path, PathBuf}, use util::ResultExt;
sync::Arc,
};
use util::{parse_env_output, ResultExt};
use collections::HashMap; use collections::HashMap;
use gpui::{AppContext, Context, Model, ModelContext, Task}; use gpui::{AppContext, Context, Model, ModelContext, Task};
@ -168,10 +165,53 @@ impl From<EnvironmentOrigin> for String {
} }
} }
#[cfg(any(test, feature = "test-support"))]
async fn load_shell_environment(
_dir: &Path,
_load_direnv: &DirenvSettings,
) -> Result<HashMap<String, String>> {
Ok([("ZED_FAKE_TEST_ENV".into(), "true".into())]
.into_iter()
.collect())
}
#[cfg(not(any(test, feature = "test-support")))]
async fn load_shell_environment( async fn load_shell_environment(
dir: &Path, dir: &Path,
load_direnv: &DirenvSettings, load_direnv: &DirenvSettings,
) -> Result<HashMap<String, String>> { ) -> Result<HashMap<String, String>> {
use anyhow::{anyhow, Context};
use std::path::PathBuf;
use util::parse_env_output;
async fn load_direnv_environment(dir: &Path) -> Result<Option<HashMap<String, String>>> {
let Ok(direnv_path) = which::which("direnv") else {
return Ok(None);
};
let direnv_output = smol::process::Command::new(direnv_path)
.args(["export", "json"])
.current_dir(dir)
.output()
.await
.context("failed to spawn direnv to get local environment variables")?;
anyhow::ensure!(
direnv_output.status.success(),
"direnv exited with error {:?}",
direnv_output.status
);
let output = String::from_utf8_lossy(&direnv_output.stdout);
if output.is_empty() {
return Ok(None);
}
Ok(Some(
serde_json::from_str(&output).context("failed to parse direnv output")?,
))
}
let direnv_environment = match load_direnv { let direnv_environment = match load_direnv {
DirenvSettings::ShellHook => None, DirenvSettings::ShellHook => None,
DirenvSettings::Direct => load_direnv_environment(dir).await?, DirenvSettings::Direct => load_direnv_environment(dir).await?,
@ -248,31 +288,3 @@ async fn load_shell_environment(
Ok(parsed_env) Ok(parsed_env)
} }
async fn load_direnv_environment(dir: &Path) -> Result<Option<HashMap<String, String>>> {
let Ok(direnv_path) = which::which("direnv") else {
return Ok(None);
};
let direnv_output = smol::process::Command::new(direnv_path)
.args(["export", "json"])
.current_dir(dir)
.output()
.await
.context("failed to spawn direnv to get local environment variables")?;
anyhow::ensure!(
direnv_output.status.success(),
"direnv exited with error {:?}",
direnv_output.status
);
let output = String::from_utf8_lossy(&direnv_output.stdout);
if output.is_empty() {
return Ok(None);
}
Ok(Some(
serde_json::from_str(&output).context("failed to parse direnv output")?,
))
}

View file

@ -4629,14 +4629,13 @@ impl LspStore {
return; return;
} }
let local = self.as_local().unwrap();
let stderr_capture = Arc::new(Mutex::new(Some(String::new()))); 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 = ProjectLspAdapterDelegate::for_local(self, worktree_handle, cx);
let cli_environment = self let cli_environment = local.environment.update(cx, |environment, cx| {
.as_local() environment.get_environment(Some(worktree_id), Some(worktree_path.clone()), cx)
.unwrap() });
.environment
.read(cx)
.get_cli_environment();
let pending_server = match self.languages.create_pending_language_server( let pending_server = match self.languages.create_pending_language_server(
stderr_capture.clone(), stderr_capture.clone(),

View file

@ -1,6 +1,6 @@
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use fs::Fs; use fs::Fs;
use gpui::{AppContext, AsyncAppContext, Context, Model, ModelContext, Task}; use gpui::{AppContext, AsyncAppContext, Context, Model, ModelContext};
use language::LanguageRegistry; use language::LanguageRegistry;
use project::{ use project::{
buffer_store::BufferStore, project_settings::SettingsObserver, search::SearchQuery, buffer_store::BufferStore, project_settings::SettingsObserver, search::SearchQuery,
@ -36,9 +36,7 @@ impl HeadlessProject {
} }
pub fn new(session: Arc<SshSession>, fs: Arc<dyn Fs>, cx: &mut ModelContext<Self>) -> Self { pub fn new(session: Arc<SshSession>, fs: Arc<dyn Fs>, cx: &mut ModelContext<Self>) -> Self {
// TODO: we should load the env correctly (as we do in login_shell_env_loaded when stdout is not a pty). Can we re-use the ProjectEnvironment for that? let mut languages = LanguageRegistry::new(cx.background_executor().clone());
let mut languages =
LanguageRegistry::new(Task::ready(()), cx.background_executor().clone());
languages languages
.set_language_server_download_dir(PathBuf::from("/Users/conrad/what-could-go-wrong")); .set_language_server_download_dir(PathBuf::from("/Users/conrad/what-could-go-wrong"));

View file

@ -321,7 +321,7 @@ fn get_language(editor: WeakView<Editor>, cx: &mut AppContext) -> Option<Arc<Lan
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use gpui::{Context, Task}; use gpui::Context;
use indoc::indoc; use indoc::indoc;
use language::{Buffer, Language, LanguageConfig, LanguageRegistry}; use language::{Buffer, Language, LanguageConfig, LanguageRegistry};
@ -475,10 +475,7 @@ mod tests {
let typescript = let typescript =
languages::language("typescript", tree_sitter_typescript::language_typescript()); languages::language("typescript", tree_sitter_typescript::language_typescript());
let python = languages::language("python", tree_sitter_python::language()); let python = languages::language("python", tree_sitter_python::language());
let language_registry = Arc::new(LanguageRegistry::new( let language_registry = Arc::new(LanguageRegistry::new(cx.background_executor().clone()));
Task::ready(()),
cx.background_executor().clone(),
));
language_registry.add(markdown.clone()); language_registry.add(markdown.clone());
language_registry.add(typescript.clone()); language_registry.add(typescript.clone());
language_registry.add(python.clone()); language_registry.add(python.clone());

View file

@ -400,16 +400,16 @@ fn main() {
paths::keymap_file().clone(), paths::keymap_file().clone(),
); );
let login_shell_env_loaded = if stdout_is_a_pty() { if !stdout_is_a_pty() {
Task::ready(()) app.background_executor()
} else { .spawn(async {
app.background_executor().spawn(async { #[cfg(unix)]
#[cfg(unix)] {
{ load_shell_from_passwd().await.log_err();
load_shell_from_passwd().await.log_err(); }
} load_login_shell_environment().await.log_err();
load_login_shell_environment().await.log_err(); })
}) .detach()
}; };
app.on_open_urls({ app.on_open_urls({
@ -451,8 +451,7 @@ fn main() {
client::init_settings(cx); client::init_settings(cx);
let client = Client::production(cx); let client = Client::production(cx);
cx.set_http_client(client.http_client().clone()); cx.set_http_client(client.http_client().clone());
let mut languages = let mut languages = LanguageRegistry::new(cx.background_executor().clone());
LanguageRegistry::new(login_shell_env_loaded, cx.background_executor().clone());
languages.set_language_server_download_dir(paths::languages_dir().clone()); languages.set_language_server_download_dir(paths::languages_dir().clone());
let languages = Arc::new(languages); let languages = Arc::new(languages);
let node_runtime = RealNodeRuntime::new(client.http_client()); let node_runtime = RealNodeRuntime::new(client.http_client());