Skip to content

Commit 42ac988

Browse files
mrnuggetas-cii
andauthored
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 #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]>
1 parent 65318cb commit 42ac988

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+371
-49
lines changed

Cargo.lock

+19-5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@ unindent = "0.1.7"
279279
url = "2.2"
280280
uuid = { version = "1.1.2", features = ["v4"] }
281281
wasmtime = "16"
282+
which = "6.0.0"
282283
sys-locale = "0.3.1"
283284

284285
[patch.crates-io]

crates/copilot/src/copilot.rs

+2
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,8 @@ impl Copilot {
428428
let binary = LanguageServerBinary {
429429
path: node_path,
430430
arguments,
431+
// TODO: We could set HTTP_PROXY etc here and fix the copilot issue.
432+
env: None,
431433
};
432434

433435
let server = LanguageServer::new(

crates/language/src/language.rs

+22
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ use serde_json::Value;
3838
use std::{
3939
any::Any,
4040
cell::RefCell,
41+
ffi::OsString,
4142
fmt::Debug,
4243
hash::Hash,
4344
mem,
@@ -140,6 +141,14 @@ impl CachedLspAdapter {
140141
})
141142
}
142143

144+
pub fn check_if_user_installed(
145+
&self,
146+
delegate: &Arc<dyn LspAdapterDelegate>,
147+
cx: &mut AsyncAppContext,
148+
) -> Option<Task<Option<LanguageServerBinary>>> {
149+
self.adapter.check_if_user_installed(delegate, cx)
150+
}
151+
143152
pub async fn fetch_latest_server_version(
144153
&self,
145154
delegate: &dyn LspAdapterDelegate,
@@ -240,6 +249,11 @@ impl CachedLspAdapter {
240249
pub trait LspAdapterDelegate: Send + Sync {
241250
fn show_notification(&self, message: &str, cx: &mut AppContext);
242251
fn http_client(&self) -> Arc<dyn HttpClient>;
252+
fn which_command(
253+
&self,
254+
command: OsString,
255+
cx: &AppContext,
256+
) -> Task<Option<(PathBuf, HashMap<String, String>)>>;
243257
}
244258

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

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

265+
fn check_if_user_installed(
266+
&self,
267+
_: &Arc<dyn LspAdapterDelegate>,
268+
_: &mut AsyncAppContext,
269+
) -> Option<Task<Option<LanguageServerBinary>>> {
270+
None
271+
}
272+
251273
async fn fetch_latest_server_version(
252274
&self,
253275
delegate: &dyn LspAdapterDelegate,

crates/language/src/language_registry.rs

+112-35
Original file line numberDiff line numberDiff line change
@@ -558,34 +558,41 @@ impl LanguageRegistry {
558558
let task = {
559559
let container_dir = container_dir.clone();
560560
cx.spawn(move |mut cx| async move {
561-
login_shell_env_loaded.await;
562-
563-
let entry = this
564-
.lsp_binary_paths
565-
.lock()
566-
.entry(adapter.name.clone())
567-
.or_insert_with(|| {
568-
let adapter = adapter.clone();
569-
let language = language.clone();
570-
let delegate = delegate.clone();
571-
cx.spawn(|cx| {
572-
get_binary(
573-
adapter,
574-
language,
575-
delegate,
576-
container_dir,
577-
lsp_binary_statuses,
578-
cx,
579-
)
580-
.map_err(Arc::new)
581-
})
582-
.shared()
583-
})
584-
.clone();
585-
586-
let binary = match entry.await {
587-
Ok(binary) => binary,
588-
Err(err) => anyhow::bail!("{err}"),
561+
// First we check whether the adapter can give us a user-installed binary.
562+
// If so, we do *not* want to cache that, because each worktree might give us a different
563+
// binary:
564+
//
565+
// worktree 1: user-installed at `.bin/gopls`
566+
// worktree 2: user-installed at `~/bin/gopls`
567+
// worktree 3: no gopls found in PATH -> fallback to Zed installation
568+
//
569+
// We only want to cache when we fall back to the global one,
570+
// because we don't want to download and overwrite our global one
571+
// for each worktree we might have open.
572+
573+
let user_binary_task = check_user_installed_binary(
574+
adapter.clone(),
575+
language.clone(),
576+
delegate.clone(),
577+
&mut cx,
578+
);
579+
let binary = if let Some(user_binary) = user_binary_task.await {
580+
user_binary
581+
} else {
582+
// If we want to install a binary globally, we need to wait for
583+
// the login shell to be set on our process.
584+
login_shell_env_loaded.await;
585+
586+
get_or_install_binary(
587+
this,
588+
&adapter,
589+
language,
590+
&delegate,
591+
&cx,
592+
container_dir,
593+
lsp_binary_statuses,
594+
)
595+
.await?
589596
};
590597

591598
if let Some(task) = adapter.will_start_server(&delegate, &mut cx) {
@@ -724,6 +731,62 @@ impl LspBinaryStatusSender {
724731
}
725732
}
726733

734+
async fn check_user_installed_binary(
735+
adapter: Arc<CachedLspAdapter>,
736+
language: Arc<Language>,
737+
delegate: Arc<dyn LspAdapterDelegate>,
738+
cx: &mut AsyncAppContext,
739+
) -> Option<LanguageServerBinary> {
740+
let Some(task) = adapter.check_if_user_installed(&delegate, cx) else {
741+
return None;
742+
};
743+
744+
task.await.and_then(|binary| {
745+
log::info!(
746+
"found user-installed language server for {}. path: {:?}, arguments: {:?}",
747+
language.name(),
748+
binary.path,
749+
binary.arguments
750+
);
751+
Some(binary)
752+
})
753+
}
754+
755+
async fn get_or_install_binary(
756+
registry: Arc<LanguageRegistry>,
757+
adapter: &Arc<CachedLspAdapter>,
758+
language: Arc<Language>,
759+
delegate: &Arc<dyn LspAdapterDelegate>,
760+
cx: &AsyncAppContext,
761+
container_dir: Arc<Path>,
762+
lsp_binary_statuses: LspBinaryStatusSender,
763+
) -> Result<LanguageServerBinary> {
764+
let entry = registry
765+
.lsp_binary_paths
766+
.lock()
767+
.entry(adapter.name.clone())
768+
.or_insert_with(|| {
769+
let adapter = adapter.clone();
770+
let language = language.clone();
771+
let delegate = delegate.clone();
772+
cx.spawn(|cx| {
773+
get_binary(
774+
adapter,
775+
language,
776+
delegate,
777+
container_dir,
778+
lsp_binary_statuses,
779+
cx,
780+
)
781+
.map_err(Arc::new)
782+
})
783+
.shared()
784+
})
785+
.clone();
786+
787+
entry.await.map_err(|err| anyhow!("{:?}", err))
788+
}
789+
727790
async fn get_binary(
728791
adapter: Arc<CachedLspAdapter>,
729792
language: Arc<Language>,
@@ -757,15 +820,20 @@ async fn get_binary(
757820
.await
758821
{
759822
statuses.send(language.clone(), LanguageServerBinaryStatus::Cached);
760-
return Ok(binary);
761-
} else {
762-
statuses.send(
763-
language.clone(),
764-
LanguageServerBinaryStatus::Failed {
765-
error: format!("{:?}", error),
766-
},
823+
log::info!(
824+
"failed to fetch newest version of language server {:?}. falling back to using {:?}",
825+
adapter.name,
826+
binary.path.display()
767827
);
828+
return Ok(binary);
768829
}
830+
831+
statuses.send(
832+
language.clone(),
833+
LanguageServerBinaryStatus::Failed {
834+
error: format!("{:?}", error),
835+
},
836+
);
769837
}
770838

771839
binary
@@ -779,14 +847,23 @@ async fn fetch_latest_binary(
779847
lsp_binary_statuses_tx: LspBinaryStatusSender,
780848
) -> Result<LanguageServerBinary> {
781849
let container_dir: Arc<Path> = container_dir.into();
850+
782851
lsp_binary_statuses_tx.send(
783852
language.clone(),
784853
LanguageServerBinaryStatus::CheckingForUpdate,
785854
);
786855

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

863+
log::info!(
864+
"checking if Zed already installed or fetching version for language server {:?}",
865+
adapter.name.0
866+
);
790867
let binary = adapter
791868
.fetch_server_binary(version_info, container_dir.to_path_buf(), delegate)
792869
.await?;

crates/lsp/src/lsp.rs

+2
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ pub enum IoKind {
5555
pub struct LanguageServerBinary {
5656
pub path: PathBuf,
5757
pub arguments: Vec<OsString>,
58+
pub env: Option<HashMap<String, String>>,
5859
}
5960

6061
/// A running language server process.
@@ -189,6 +190,7 @@ impl LanguageServer {
189190
let mut server = process::Command::new(&binary.path)
190191
.current_dir(working_dir)
191192
.args(binary.arguments)
193+
.envs(binary.env.unwrap_or_default())
192194
.stdin(Stdio::piped())
193195
.stdout(Stdio::piped())
194196
.stderr(Stdio::piped())

crates/prettier/src/prettier.rs

+1
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ impl Prettier {
192192
LanguageServerBinary {
193193
path: node_path,
194194
arguments: vec![prettier_server.into(), prettier_dir.as_path().into()],
195+
env: None,
195196
},
196197
Path::new("/"),
197198
None,

crates/project/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ text.workspace = true
6565
thiserror.workspace = true
6666
toml.workspace = true
6767
util.workspace = true
68+
which.workspace = true
6869

6970
[dev-dependencies]
7071
client = { workspace = true, features = ["test-support"] }

0 commit comments

Comments
 (0)