Skip to content

Commit

Permalink
Detect and possibly use user-installed gopls / zls language serve…
Browse files Browse the repository at this point in the history
…rs (#8188)

After a lot of back-and-forth, this is a small attempt to implement
solutions (1) and (3) in
#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:

![screenshot-2024-02-23-11 14
08@2x](https://github.com/zed-industries/zed/assets/1185253/822ea59b-c63e-4102-a50e-75501cc4e0e3)

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 <[email protected]>
  • Loading branch information
mrnugget and as-cii committed Feb 23, 2024
1 parent 57426b9 commit 10df9df
Show file tree
Hide file tree
Showing 42 changed files with 371 additions and 49 deletions.
24 changes: 19 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ unindent = "0.1.7"
url = "2.2"
uuid = { version = "1.1.2", features = ["v4"] }
wasmtime = "16"
which = "6.0.0"
sys-locale = "0.3.1"

[patch.crates-io]
Expand Down
2 changes: 2 additions & 0 deletions crates/copilot/src/copilot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,8 @@ impl Copilot {
let binary = LanguageServerBinary {
path: node_path,
arguments,
// TODO: We could set HTTP_PROXY etc here and fix the copilot issue.
env: None,
};

let server = LanguageServer::new(
Expand Down
22 changes: 22 additions & 0 deletions crates/language/src/language.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ use serde_json::Value;
use std::{
any::Any,
cell::RefCell,
ffi::OsString,
fmt::Debug,
hash::Hash,
mem,
Expand Down Expand Up @@ -140,6 +141,14 @@ impl CachedLspAdapter {
})
}

pub fn check_if_user_installed(
&self,
delegate: &Arc<dyn LspAdapterDelegate>,
cx: &mut AsyncAppContext,
) -> Option<Task<Option<LanguageServerBinary>>> {
self.adapter.check_if_user_installed(delegate, cx)
}

pub async fn fetch_latest_server_version(
&self,
delegate: &dyn LspAdapterDelegate,
Expand Down Expand Up @@ -240,6 +249,11 @@ impl CachedLspAdapter {
pub trait LspAdapterDelegate: Send + Sync {
fn show_notification(&self, message: &str, cx: &mut AppContext);
fn http_client(&self) -> Arc<dyn HttpClient>;
fn which_command(
&self,
command: OsString,
cx: &AppContext,
) -> Task<Option<(PathBuf, HashMap<String, String>)>>;
}

#[async_trait]
Expand All @@ -248,6 +262,14 @@ pub trait LspAdapter: 'static + Send + Sync {

fn short_name(&self) -> &'static str;

fn check_if_user_installed(
&self,
_: &Arc<dyn LspAdapterDelegate>,
_: &mut AsyncAppContext,
) -> Option<Task<Option<LanguageServerBinary>>> {
None
}

async fn fetch_latest_server_version(
&self,
delegate: &dyn LspAdapterDelegate,
Expand Down
147 changes: 112 additions & 35 deletions crates/language/src/language_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -558,34 +558,41 @@ impl LanguageRegistry {
let task = {
let container_dir = container_dir.clone();
cx.spawn(move |mut cx| async move {
login_shell_env_loaded.await;

let entry = this
.lsp_binary_paths
.lock()
.entry(adapter.name.clone())
.or_insert_with(|| {
let adapter = adapter.clone();
let language = language.clone();
let delegate = delegate.clone();
cx.spawn(|cx| {
get_binary(
adapter,
language,
delegate,
container_dir,
lsp_binary_statuses,
cx,
)
.map_err(Arc::new)
})
.shared()
})
.clone();

let binary = match entry.await {
Ok(binary) => binary,
Err(err) => anyhow::bail!("{err}"),
// First we check whether the adapter can give us a user-installed binary.
// If so, we do *not* want to cache that, because each worktree might give us a different
// binary:
//
// worktree 1: user-installed at `.bin/gopls`
// worktree 2: user-installed at `~/bin/gopls`
// worktree 3: no gopls found in PATH -> fallback to Zed installation
//
// We only want to cache when we fall back to the global one,
// because we don't want to download and overwrite our global one
// for each worktree we might have open.

let user_binary_task = check_user_installed_binary(
adapter.clone(),
language.clone(),
delegate.clone(),
&mut cx,
);
let binary = if let Some(user_binary) = user_binary_task.await {
user_binary
} else {
// If we want to install a binary globally, we need to wait for
// the login shell to be set on our process.
login_shell_env_loaded.await;

get_or_install_binary(
this,
&adapter,
language,
&delegate,
&cx,
container_dir,
lsp_binary_statuses,
)
.await?
};

if let Some(task) = adapter.will_start_server(&delegate, &mut cx) {
Expand Down Expand Up @@ -724,6 +731,62 @@ impl LspBinaryStatusSender {
}
}

async fn check_user_installed_binary(
adapter: Arc<CachedLspAdapter>,
language: Arc<Language>,
delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut AsyncAppContext,
) -> Option<LanguageServerBinary> {
let Some(task) = adapter.check_if_user_installed(&delegate, cx) else {
return None;
};

task.await.and_then(|binary| {
log::info!(
"found user-installed language server for {}. path: {:?}, arguments: {:?}",
language.name(),
binary.path,
binary.arguments
);
Some(binary)
})
}

async fn get_or_install_binary(
registry: Arc<LanguageRegistry>,
adapter: &Arc<CachedLspAdapter>,
language: Arc<Language>,
delegate: &Arc<dyn LspAdapterDelegate>,
cx: &AsyncAppContext,
container_dir: Arc<Path>,
lsp_binary_statuses: LspBinaryStatusSender,
) -> Result<LanguageServerBinary> {
let entry = registry
.lsp_binary_paths
.lock()
.entry(adapter.name.clone())
.or_insert_with(|| {
let adapter = adapter.clone();
let language = language.clone();
let delegate = delegate.clone();
cx.spawn(|cx| {
get_binary(
adapter,
language,
delegate,
container_dir,
lsp_binary_statuses,
cx,
)
.map_err(Arc::new)
})
.shared()
})
.clone();

entry.await.map_err(|err| anyhow!("{:?}", err))
}

async fn get_binary(
adapter: Arc<CachedLspAdapter>,
language: Arc<Language>,
Expand Down Expand Up @@ -757,15 +820,20 @@ async fn get_binary(
.await
{
statuses.send(language.clone(), LanguageServerBinaryStatus::Cached);
return Ok(binary);
} else {
statuses.send(
language.clone(),
LanguageServerBinaryStatus::Failed {
error: format!("{:?}", error),
},
log::info!(
"failed to fetch newest version of language server {:?}. falling back to using {:?}",
adapter.name,
binary.path.display()
);
return Ok(binary);
}

statuses.send(
language.clone(),
LanguageServerBinaryStatus::Failed {
error: format!("{:?}", error),
},
);
}

binary
Expand All @@ -779,14 +847,23 @@ async fn fetch_latest_binary(
lsp_binary_statuses_tx: LspBinaryStatusSender,
) -> Result<LanguageServerBinary> {
let container_dir: Arc<Path> = container_dir.into();

lsp_binary_statuses_tx.send(
language.clone(),
LanguageServerBinaryStatus::CheckingForUpdate,
);

log::info!(
"querying GitHub for latest version of language server {:?}",
adapter.name.0
);
let version_info = adapter.fetch_latest_server_version(delegate).await?;
lsp_binary_statuses_tx.send(language.clone(), LanguageServerBinaryStatus::Downloading);

log::info!(
"checking if Zed already installed or fetching version for language server {:?}",
adapter.name.0
);
let binary = adapter
.fetch_server_binary(version_info, container_dir.to_path_buf(), delegate)
.await?;
Expand Down
2 changes: 2 additions & 0 deletions crates/lsp/src/lsp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ pub enum IoKind {
pub struct LanguageServerBinary {
pub path: PathBuf,
pub arguments: Vec<OsString>,
pub env: Option<HashMap<String, String>>,
}

/// A running language server process.
Expand Down Expand Up @@ -189,6 +190,7 @@ impl LanguageServer {
let mut server = process::Command::new(&binary.path)
.current_dir(working_dir)
.args(binary.arguments)
.envs(binary.env.unwrap_or_default())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
Expand Down
1 change: 1 addition & 0 deletions crates/prettier/src/prettier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ impl Prettier {
LanguageServerBinary {
path: node_path,
arguments: vec![prettier_server.into(), prettier_dir.as_path().into()],
env: None,
},
Path::new("/"),
None,
Expand Down
1 change: 1 addition & 0 deletions crates/project/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ text.workspace = true
thiserror.workspace = true
toml.workspace = true
util.workspace = true
which.workspace = true

[dev-dependencies]
client = { workspace = true, features = ["test-support"] }
Expand Down
Loading

0 comments on commit 10df9df

Please sign in to comment.