Detect and possibly use user-installed gopls
/ zls
language servers (#8188)
After a lot of back-and-forth, this is a small attempt to implement solutions (1) and (3) in https://github.com/zed-industries/zed/issues/7902. The goal is to have a minimal change that helps users get started with Zed, until we have extensions ready. Release Notes: - Added detection of user-installed `gopls` to Go language server adapter. If a user has `gopls` in `$PATH` when opening a worktree, it will be used. - Added detection of user-installed `zls` to Zig language server adapter. If a user has `zls` in `$PATH` when opening a worktree, it will be used. Example: I don't have `go` installed globally, but I do have `gopls`: ``` ~ $ which go go not found ~ $ which gopls /Users/thorstenball/code/go/bin/gopls ``` But I do have `go` in a project's directory: ``` ~/tmp/go-testing φ which go /Users/thorstenball/.local/share/mise/installs/go/1.21.5/go/bin/go ~/tmp/go-testing φ which gopls /Users/thorstenball/code/go/bin/gopls ``` With current Zed when I run `zed ~/tmp/go-testing`, I'd get the dreaded error:  But with the changes in this PR, it works: ``` [2024-02-23T11:14:42+01:00 INFO language::language_registry] starting language server "gopls", path: "/Users/thorstenball/tmp/go-testing", id: 1 [2024-02-23T11:14:42+01:00 INFO language::language_registry] found user-installed language server for Go. path: "/Users/thorstenball/code/go/bin/gopls", arguments: ["-mode=stdio"] [2024-02-23T11:14:42+01:00 INFO lsp] starting language server. binary path: "/Users/thorstenball/code/go/bin/gopls", working directory: "/Users/thorstenball/tmp/go-testing", args: ["-mode=stdio"] ``` --------- Co-authored-by: Antonio <antonio@zed.dev>
This commit is contained in:
parent
65318cb6ac
commit
42ac9880c6
42 changed files with 369 additions and 47 deletions
|
@ -71,6 +71,8 @@ use smol::lock::Semaphore;
|
|||
use std::{
|
||||
cmp::{self, Ordering},
|
||||
convert::TryInto,
|
||||
env,
|
||||
ffi::OsString,
|
||||
hash::Hash,
|
||||
mem,
|
||||
num::NonZeroU32,
|
||||
|
@ -504,11 +506,6 @@ pub enum FormatTrigger {
|
|||
Manual,
|
||||
}
|
||||
|
||||
struct ProjectLspAdapterDelegate {
|
||||
project: Model<Project>,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
}
|
||||
|
||||
// Currently, formatting operations are represented differently depending on
|
||||
// whether they come from a language server or an external command.
|
||||
enum FormatOperation {
|
||||
|
@ -2803,7 +2800,7 @@ impl Project {
|
|||
|
||||
fn start_language_server(
|
||||
&mut self,
|
||||
worktree: &Model<Worktree>,
|
||||
worktree_handle: &Model<Worktree>,
|
||||
adapter: Arc<CachedLspAdapter>,
|
||||
language: Arc<Language>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
|
@ -2812,7 +2809,7 @@ impl Project {
|
|||
return;
|
||||
}
|
||||
|
||||
let worktree = worktree.read(cx);
|
||||
let worktree = worktree_handle.read(cx);
|
||||
let worktree_id = worktree.id();
|
||||
let worktree_path = worktree.abs_path();
|
||||
let key = (worktree_id, adapter.name.clone());
|
||||
|
@ -2826,7 +2823,7 @@ impl Project {
|
|||
language.clone(),
|
||||
adapter.clone(),
|
||||
Arc::clone(&worktree_path),
|
||||
ProjectLspAdapterDelegate::new(self, cx),
|
||||
ProjectLspAdapterDelegate::new(self, worktree_handle, cx),
|
||||
cx,
|
||||
) {
|
||||
Some(pending_server) => pending_server,
|
||||
|
@ -9298,10 +9295,17 @@ impl<P: AsRef<Path>> From<(WorktreeId, P)> for ProjectPath {
|
|||
}
|
||||
}
|
||||
|
||||
struct ProjectLspAdapterDelegate {
|
||||
project: Model<Project>,
|
||||
worktree: Model<Worktree>,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
}
|
||||
|
||||
impl ProjectLspAdapterDelegate {
|
||||
fn new(project: &Project, cx: &ModelContext<Project>) -> Arc<Self> {
|
||||
fn new(project: &Project, worktree: &Model<Worktree>, cx: &ModelContext<Project>) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
project: cx.handle(),
|
||||
worktree: worktree.clone(),
|
||||
http_client: project.client.http_client(),
|
||||
})
|
||||
}
|
||||
|
@ -9316,6 +9320,41 @@ impl LspAdapterDelegate for ProjectLspAdapterDelegate {
|
|||
fn http_client(&self) -> Arc<dyn HttpClient> {
|
||||
self.http_client.clone()
|
||||
}
|
||||
|
||||
fn which_command(
|
||||
&self,
|
||||
command: OsString,
|
||||
cx: &AppContext,
|
||||
) -> Task<Option<(PathBuf, HashMap<String, String>)>> {
|
||||
let worktree_abs_path = self.worktree.read(cx).abs_path();
|
||||
let command = command.to_owned();
|
||||
|
||||
cx.background_executor().spawn(async move {
|
||||
let shell_env = load_shell_environment(&worktree_abs_path)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to determine load login shell environment in {worktree_abs_path:?}"
|
||||
)
|
||||
})
|
||||
.log_err();
|
||||
|
||||
if let Some(shell_env) = shell_env {
|
||||
let shell_path = shell_env.get("PATH");
|
||||
match which::which_in(&command, shell_path, &worktree_abs_path) {
|
||||
Ok(command_path) => Some((command_path, shell_env)),
|
||||
Err(error) => {
|
||||
log::warn!(
|
||||
"failed to determine path for command {:?} in env {shell_env:?}: {error}", command.to_string_lossy()
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_symbol(symbol: &Symbol) -> proto::Symbol {
|
||||
|
@ -9423,3 +9462,55 @@ fn include_text(server: &lsp::LanguageServer) -> bool {
|
|||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
async fn load_shell_environment(dir: &Path) -> Result<HashMap<String, String>> {
|
||||
let marker = "ZED_SHELL_START";
|
||||
let shell = env::var("SHELL").context(
|
||||
"SHELL environment variable is not assigned so we can't source login environment variables",
|
||||
)?;
|
||||
let output = smol::process::Command::new(&shell)
|
||||
.args([
|
||||
"-i",
|
||||
"-c",
|
||||
// What we're doing here is to spawn a shell and then `cd` into
|
||||
// the project directory to get the env in there as if the user
|
||||
// `cd`'d into it. We do that because tools like direnv, asdf, ...
|
||||
// hook into `cd` and only set up the env after that.
|
||||
//
|
||||
// The `exit 0` is the result of hours of debugging, trying to find out
|
||||
// why running this command here, without `exit 0`, would mess
|
||||
// up signal process for our process so that `ctrl-c` doesn't work
|
||||
// anymore.
|
||||
// We still don't know why `$SHELL -l -i -c '/usr/bin/env -0'` would
|
||||
// do that, but it does, and `exit 0` helps.
|
||||
&format!("cd {dir:?}; echo {marker}; /usr/bin/env -0; exit 0;"),
|
||||
])
|
||||
.output()
|
||||
.await
|
||||
.context("failed to spawn login shell to source login environment variables")?;
|
||||
|
||||
anyhow::ensure!(
|
||||
output.status.success(),
|
||||
"login shell exited with error {:?}",
|
||||
output.status
|
||||
);
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let env_output_start = stdout.find(marker).ok_or_else(|| {
|
||||
anyhow!(
|
||||
"failed to parse output of `env` command in login shell: {}",
|
||||
stdout
|
||||
)
|
||||
})?;
|
||||
|
||||
let mut parsed_env = HashMap::default();
|
||||
let env_output = &stdout[env_output_start + marker.len()..];
|
||||
for line in env_output.split_terminator('\0') {
|
||||
if let Some(separator_index) = line.find('=') {
|
||||
let key = line[..separator_index].to_string();
|
||||
let value = line[separator_index + 1..].to_string();
|
||||
parsed_env.insert(key, value);
|
||||
}
|
||||
}
|
||||
Ok(parsed_env)
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue