Cargo.lock 🔗
@@ -4239,6 +4239,7 @@ dependencies = [
"async-tar",
"futures 0.3.28",
"gpui",
+ "log",
"parking_lot 0.11.2",
"serde",
"serde_derive",
Julia created
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Closes
https://linear.app/zed-industries/issue/Z-665/add-a-mechanism-for-detecting-and-fixing-broken-language-server
Fixes https://github.com/zed-industries/community/issues/1671
Fixes https://github.com/zed-industries/community/issues/1691
Fixes https://github.com/zed-industries/community/issues/1524
Fixes https://github.com/zed-industries/community/issues/1352
Fixes https://github.com/zed-industries/community/issues/1109
Fixes https://github.com/zed-industries/community/issues/996
Fixes https://github.com/zed-industries/community/issues/782
Things this PR does:
- Updates our elixir-ls fetching to use new release name format
- Detect when a server fails to launch
- If the adapter claims to be reinstallable, get a test binary
- If the test binary fails to launch or returns a failure error code
- Clear container dir and reinstall
- Detect/fix broken Node
Things it does not do:
- Restart server on failure, I have most of the stuff for this already
so it should be a fast follow up
- Detect/fix broken Copilot
Node and Copilot shouldn't be too bad, they are handled via different
mechanisms. Originally I put effort into detecting failure of the server
during normal operation post launch, but that's not really needed. If
the server gets borked while running then we'll catch that on next
startup. Realizing that allowed for pruning a bunch of the work I did
and made the overall system a lot nicer
Copilot is nominally a language server but does not have an adapter and
does not run through the same mechanism in the project.
We're going to have an issue with multiple language server instances in
different projects once we add a "Reinstall Language Server" action,
which is why it's not in this PR. Each project has its own list of
server instances and is currently vaguely responsible for managing the
installations which means they can step on each others toes. This should
change, probably
Release Notes:
- Added a mechanism to detect and reinstall broken language servers
([#1691](https://github.com/zed-industries/community/issues/1691))
([#1524](https://github.com/zed-industries/community/issues/1524))
([#1352](https://github.com/zed-industries/community/issues/1352))
([#1109](https://github.com/zed-industries/community/issues/1109))
([#996](https://github.com/zed-industries/community/issues/996))
([#782](https://github.com/zed-industries/community/issues/782)).
Cargo.lock | 1
crates/activity_indicator/src/activity_indicator.rs | 13
crates/copilot/src/copilot.rs | 13
crates/language/src/language.rs | 174 +
crates/lsp/src/lsp.rs | 23
crates/node_runtime/Cargo.toml | 1
crates/node_runtime/src/node_runtime.rs | 203 +-
crates/project/src/project.rs | 949 +++++++++-----
crates/util/src/util.rs | 14
crates/zed/src/languages/c.rs | 67
crates/zed/src/languages/elixir.rs | 41
crates/zed/src/languages/go.rs | 67
crates/zed/src/languages/html.rs | 75
crates/zed/src/languages/json.rs | 69
crates/zed/src/languages/language_plugin.rs | 11
crates/zed/src/languages/lua.rs | 69
crates/zed/src/languages/python.rs | 73
crates/zed/src/languages/ruby.rs | 11
crates/zed/src/languages/rust.rs | 40
crates/zed/src/languages/typescript.rs | 109 +
crates/zed/src/languages/yaml.rs | 73
crates/zed/src/main.rs | 2
crates/zed/src/zed.rs | 2
23 files changed, 1,309 insertions(+), 791 deletions(-)
@@ -4239,6 +4239,7 @@ dependencies = [
"async-tar",
"futures 0.3.28",
"gpui",
+ "log",
"parking_lot 0.11.2",
"serde",
"serde_derive",
@@ -207,16 +207,11 @@ impl ActivityIndicator {
let mut checking_for_update = SmallVec::<[_; 3]>::new();
let mut failed = SmallVec::<[_; 3]>::new();
for status in &self.statuses {
+ let name = status.name.clone();
match status.status {
- LanguageServerBinaryStatus::CheckingForUpdate => {
- checking_for_update.push(status.name.clone());
- }
- LanguageServerBinaryStatus::Downloading => {
- downloading.push(status.name.clone());
- }
- LanguageServerBinaryStatus::Failed { .. } => {
- failed.push(status.name.clone());
- }
+ LanguageServerBinaryStatus::CheckingForUpdate => checking_for_update.push(name),
+ LanguageServerBinaryStatus::Downloading => downloading.push(name),
+ LanguageServerBinaryStatus::Failed { .. } => failed.push(name),
LanguageServerBinaryStatus::Downloaded | LanguageServerBinaryStatus::Cached => {}
}
}
@@ -15,7 +15,7 @@ use language::{
ToPointUtf16,
};
use log::{debug, error};
-use lsp::{LanguageServer, LanguageServerId};
+use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId};
use node_runtime::NodeRuntime;
use request::{LogMessage, StatusNotification};
use settings::SettingsStore;
@@ -340,7 +340,7 @@ impl Copilot {
let http = util::http::FakeHttpClient::create(|_| async { unreachable!() });
let this = cx.add_model(|cx| Self {
http: http.clone(),
- node_runtime: NodeRuntime::new(http, cx.background().clone()),
+ node_runtime: NodeRuntime::instance(http, cx.background().clone()),
server: CopilotServer::Running(RunningCopilotServer {
lsp: Arc::new(server),
sign_in_status: SignInStatus::Authorized,
@@ -361,11 +361,14 @@ impl Copilot {
let start_language_server = async {
let server_path = get_copilot_lsp(http).await?;
let node_path = node_runtime.binary_path().await?;
- let arguments: &[OsString] = &[server_path.into(), "--stdio".into()];
+ let arguments: Vec<OsString> = vec![server_path.into(), "--stdio".into()];
+ let binary = LanguageServerBinary {
+ path: node_path,
+ arguments,
+ };
let server = LanguageServer::new(
LanguageServerId(0),
- &node_path,
- arguments,
+ binary,
Path::new("/"),
None,
cx.clone(),
@@ -20,7 +20,7 @@ use futures::{
use gpui::{executor::Background, AppContext, AsyncAppContext, Task};
use highlight_map::HighlightMap;
use lazy_static::lazy_static;
-use lsp::CodeActionKind;
+use lsp::{CodeActionKind, LanguageServerBinary};
use parking_lot::{Mutex, RwLock};
use postage::watch;
use regex::Regex;
@@ -30,7 +30,6 @@ use std::{
any::Any,
borrow::Cow,
cell::RefCell,
- ffi::OsString,
fmt::Debug,
hash::Hash,
mem,
@@ -86,12 +85,6 @@ pub trait ToLspPosition {
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct LanguageServerName(pub Arc<str>);
-#[derive(Debug, Clone, Deserialize)]
-pub struct LanguageServerBinary {
- pub path: PathBuf,
- pub arguments: Vec<OsString>,
-}
-
/// Represents a Language Server, with certain cached sync properties.
/// Uses [`LspAdapter`] under the hood, but calls all 'static' methods
/// once at startup, and caches the results.
@@ -167,6 +160,17 @@ impl CachedLspAdapter {
.await
}
+ pub fn can_be_reinstalled(&self) -> bool {
+ self.adapter.can_be_reinstalled()
+ }
+
+ pub async fn installation_test_binary(
+ &self,
+ container_dir: PathBuf,
+ ) -> Option<LanguageServerBinary> {
+ self.adapter.installation_test_binary(container_dir).await
+ }
+
pub fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
self.adapter.code_action_kinds()
}
@@ -249,6 +253,15 @@ pub trait LspAdapter: 'static + Send + Sync {
delegate: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary>;
+ fn can_be_reinstalled(&self) -> bool {
+ true
+ }
+
+ async fn installation_test_binary(
+ &self,
+ container_dir: PathBuf,
+ ) -> Option<LanguageServerBinary>;
+
async fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
async fn process_completion(&self, _: &mut lsp::CompletionItem) {}
@@ -576,7 +589,8 @@ struct LanguageRegistryState {
pub struct PendingLanguageServer {
pub server_id: LanguageServerId,
- pub task: Task<Result<lsp::LanguageServer>>,
+ pub task: Task<Result<Option<lsp::LanguageServer>>>,
+ pub container_dir: Option<Arc<Path>>,
}
impl LanguageRegistry {
@@ -848,7 +862,7 @@ impl LanguageRegistry {
self.state.read().languages.iter().cloned().collect()
}
- pub fn start_language_server(
+ pub fn create_pending_language_server(
self: &Arc<Self>,
language: Arc<Language>,
adapter: Arc<CachedLspAdapter>,
@@ -858,7 +872,7 @@ impl LanguageRegistry {
) -> Option<PendingLanguageServer> {
let server_id = self.state.write().next_language_server_id();
log::info!(
- "starting language server name:{}, path:{root_path:?}, id:{server_id}",
+ "starting language server {:?}, path: {root_path:?}, id: {server_id}",
adapter.name.0
);
@@ -888,66 +902,81 @@ impl LanguageRegistry {
}
})
.detach();
- Ok(server)
+
+ Ok(Some(server))
});
- return Some(PendingLanguageServer { server_id, task });
+ return Some(PendingLanguageServer {
+ server_id,
+ task,
+ container_dir: None,
+ });
}
let download_dir = self
.language_server_download_dir
.clone()
- .ok_or_else(|| anyhow!("language server download directory has not been assigned"))
+ .ok_or_else(|| anyhow!("language server download directory has not been assigned before starting server"))
.log_err()?;
let this = self.clone();
let language = language.clone();
- let download_dir = download_dir.clone();
+ let container_dir: Arc<Path> = Arc::from(download_dir.join(adapter.name.0.as_ref()));
let root_path = root_path.clone();
let adapter = adapter.clone();
let lsp_binary_statuses = self.lsp_binary_statuses_tx.clone();
let login_shell_env_loaded = self.login_shell_env_loaded.clone();
- let task = cx.spawn(|mut cx| async move {
- login_shell_env_loaded.await;
-
- let entry = this
- .lsp_binary_paths
- .lock()
- .entry(adapter.name.clone())
- .or_insert_with(|| {
- cx.spawn(|cx| {
- get_binary(
- adapter.clone(),
- language.clone(),
- delegate.clone(),
- download_dir,
- lsp_binary_statuses,
- cx,
- )
- .map_err(Arc::new)
+ let task = {
+ let container_dir = container_dir.clone();
+ cx.spawn(|mut cx| async move {
+ login_shell_env_loaded.await;
+
+ let mut lock = this.lsp_binary_paths.lock();
+ let entry = lock
+ .entry(adapter.name.clone())
+ .or_insert_with(|| {
+ cx.spawn(|cx| {
+ get_binary(
+ adapter.clone(),
+ language.clone(),
+ delegate.clone(),
+ container_dir,
+ lsp_binary_statuses,
+ cx,
+ )
+ .map_err(Arc::new)
+ })
+ .shared()
})
- .shared()
- })
- .clone();
- let binary = entry.clone().map_err(|e| anyhow!(e)).await?;
+ .clone();
+ drop(lock);
- if let Some(task) = adapter.will_start_server(&delegate, &mut cx) {
- task.await?;
- }
+ let binary = match entry.clone().await.log_err() {
+ Some(binary) => binary,
+ None => return Ok(None),
+ };
- let server = lsp::LanguageServer::new(
- server_id,
- &binary.path,
- &binary.arguments,
- &root_path,
- adapter.code_action_kinds(),
- cx,
- )?;
-
- Ok(server)
- });
+ if let Some(task) = adapter.will_start_server(&delegate, &mut cx) {
+ if task.await.log_err().is_none() {
+ return Ok(None);
+ }
+ }
+
+ Ok(Some(lsp::LanguageServer::new(
+ server_id,
+ binary,
+ &root_path,
+ adapter.code_action_kinds(),
+ cx,
+ )?))
+ })
+ };
- Some(PendingLanguageServer { server_id, task })
+ Some(PendingLanguageServer {
+ server_id,
+ task,
+ container_dir: Some(container_dir),
+ })
}
pub fn language_server_binary_statuses(
@@ -955,6 +984,30 @@ impl LanguageRegistry {
) -> async_broadcast::Receiver<(Arc<Language>, LanguageServerBinaryStatus)> {
self.lsp_binary_statuses_rx.clone()
}
+
+ pub fn delete_server_container(
+ &self,
+ adapter: Arc<CachedLspAdapter>,
+ cx: &mut AppContext,
+ ) -> Task<()> {
+ log::info!("deleting server container");
+
+ let mut lock = self.lsp_binary_paths.lock();
+ lock.remove(&adapter.name);
+
+ let download_dir = self
+ .language_server_download_dir
+ .clone()
+ .expect("language server download directory has not been assigned before deleting server container");
+
+ cx.spawn(|_| async move {
+ let container_dir = download_dir.join(adapter.name.0.as_ref());
+ smol::fs::remove_dir_all(container_dir)
+ .await
+ .context("server container removal")
+ .log_err();
+ })
+ }
}
impl LanguageRegistryState {
@@ -1005,11 +1058,10 @@ async fn get_binary(
adapter: Arc<CachedLspAdapter>,
language: Arc<Language>,
delegate: Arc<dyn LspAdapterDelegate>,
- download_dir: Arc<Path>,
+ container_dir: Arc<Path>,
statuses: async_broadcast::Sender<(Arc<Language>, LanguageServerBinaryStatus)>,
mut cx: AsyncAppContext,
) -> Result<LanguageServerBinary> {
- let container_dir = download_dir.join(adapter.name.0.as_ref());
if !container_dir.exists() {
smol::fs::create_dir_all(&container_dir)
.await
@@ -1030,14 +1082,14 @@ async fn get_binary(
.await;
if let Err(error) = binary.as_ref() {
- if let Some(cached) = adapter
- .cached_server_binary(container_dir, delegate.as_ref())
+ if let Some(binary) = adapter
+ .cached_server_binary(container_dir.to_path_buf(), delegate.as_ref())
.await
{
statuses
.broadcast((language.clone(), LanguageServerBinaryStatus::Cached))
.await?;
- return Ok(cached);
+ return Ok(binary);
} else {
statuses
.broadcast((
@@ -1049,6 +1101,7 @@ async fn get_binary(
.await?;
}
}
+
binary
}
@@ -1066,16 +1119,19 @@ async fn fetch_latest_binary(
LanguageServerBinaryStatus::CheckingForUpdate,
))
.await?;
+
let version_info = adapter.fetch_latest_server_version(delegate).await?;
lsp_binary_statuses_tx
.broadcast((language.clone(), LanguageServerBinaryStatus::Downloading))
.await?;
+
let binary = adapter
.fetch_server_binary(version_info, container_dir.to_path_buf(), delegate)
.await?;
lsp_binary_statuses_tx
.broadcast((language.clone(), LanguageServerBinaryStatus::Downloaded))
.await?;
+
Ok(binary)
}
@@ -1617,6 +1673,10 @@ impl LspAdapter for Arc<FakeLspAdapter> {
unreachable!();
}
+ async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
+ unreachable!();
+ }
+
async fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
async fn disk_based_diagnostic_sources(&self) -> Vec<String> {
@@ -16,6 +16,7 @@ use smol::{
process::{self, Child},
};
use std::{
+ ffi::OsString,
fmt,
future::Future,
io::Write,
@@ -36,6 +37,12 @@ type NotificationHandler = Box<dyn Send + FnMut(Option<usize>, &str, AsyncAppCon
type ResponseHandler = Box<dyn Send + FnOnce(Result<String, Error>)>;
type IoHandler = Box<dyn Send + FnMut(bool, &str)>;
+#[derive(Debug, Clone, Deserialize)]
+pub struct LanguageServerBinary {
+ pub path: PathBuf,
+ pub arguments: Vec<OsString>,
+}
+
pub struct LanguageServer {
server_id: LanguageServerId,
next_id: AtomicUsize,
@@ -51,7 +58,7 @@ pub struct LanguageServer {
io_tasks: Mutex<Option<(Task<Option<()>>, Task<Option<()>>)>>,
output_done_rx: Mutex<Option<barrier::Receiver>>,
root_path: PathBuf,
- _server: Option<Child>,
+ _server: Option<Mutex<Child>>,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
@@ -119,10 +126,9 @@ struct Error {
}
impl LanguageServer {
- pub fn new<T: AsRef<std::ffi::OsStr>>(
+ pub fn new(
server_id: LanguageServerId,
- binary_path: &Path,
- arguments: &[T],
+ binary: LanguageServerBinary,
root_path: &Path,
code_action_kinds: Option<Vec<CodeActionKind>>,
cx: AsyncAppContext,
@@ -133,9 +139,9 @@ impl LanguageServer {
root_path.parent().unwrap_or_else(|| Path::new("/"))
};
- let mut server = process::Command::new(binary_path)
+ let mut server = process::Command::new(&binary.path)
.current_dir(working_dir)
- .args(arguments)
+ .args(binary.arguments)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
@@ -167,9 +173,10 @@ impl LanguageServer {
},
);
- if let Some(name) = binary_path.file_name() {
+ if let Some(name) = binary.path.file_name() {
server.name = name.to_string_lossy().to_string();
}
+
Ok(server)
}
@@ -231,7 +238,7 @@ impl LanguageServer {
io_tasks: Mutex::new(Some((input_task, output_task))),
output_done_rx: Mutex::new(Some(output_done_rx)),
root_path: root_path.to_path_buf(),
- _server: server,
+ _server: server.map(|server| Mutex::new(server)),
}
}
@@ -20,3 +20,4 @@ serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
smol.workspace = true
+log.workspace = true
@@ -1,21 +1,24 @@
use anyhow::{anyhow, bail, Context, Result};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
+use futures::lock::Mutex;
use futures::{future::Shared, FutureExt};
use gpui::{executor::Background, Task};
-use parking_lot::Mutex;
use serde::Deserialize;
use smol::{fs, io::BufReader, process::Command};
+use std::process::Output;
use std::{
env::consts,
path::{Path, PathBuf},
- sync::Arc,
+ sync::{Arc, OnceLock},
};
-use util::http::HttpClient;
+use util::{http::HttpClient, ResultExt};
const VERSION: &str = "v18.15.0";
-#[derive(Deserialize)]
+static RUNTIME_INSTANCE: OnceLock<Arc<NodeRuntime>> = OnceLock::new();
+
+#[derive(Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct NpmInfo {
#[serde(default)]
@@ -23,7 +26,7 @@ pub struct NpmInfo {
versions: Vec<String>,
}
-#[derive(Deserialize, Default)]
+#[derive(Debug, Deserialize, Default)]
pub struct NpmInfoDistTags {
latest: Option<String>,
}
@@ -35,12 +38,16 @@ pub struct NodeRuntime {
}
impl NodeRuntime {
- pub fn new(http: Arc<dyn HttpClient>, background: Arc<Background>) -> Arc<NodeRuntime> {
- Arc::new(NodeRuntime {
- http,
- background,
- installation_path: Mutex::new(None),
- })
+ pub fn instance(http: Arc<dyn HttpClient>, background: Arc<Background>) -> Arc<NodeRuntime> {
+ RUNTIME_INSTANCE
+ .get_or_init(|| {
+ Arc::new(NodeRuntime {
+ http,
+ background,
+ installation_path: Mutex::new(None),
+ })
+ })
+ .clone()
}
pub async fn binary_path(&self) -> Result<PathBuf> {
@@ -50,55 +57,74 @@ impl NodeRuntime {
pub async fn run_npm_subcommand(
&self,
- directory: &Path,
+ directory: Option<&Path>,
subcommand: &str,
args: &[&str],
- ) -> Result<()> {
+ ) -> Result<Output> {
+ let attempt = |installation_path: PathBuf| async move {
+ let node_binary = installation_path.join("bin/node");
+ let npm_file = installation_path.join("bin/npm");
+
+ if smol::fs::metadata(&node_binary).await.is_err() {
+ return Err(anyhow!("missing node binary file"));
+ }
+
+ if smol::fs::metadata(&npm_file).await.is_err() {
+ return Err(anyhow!("missing npm file"));
+ }
+
+ let mut command = Command::new(node_binary);
+ command.arg(npm_file).arg(subcommand).args(args);
+
+ if let Some(directory) = directory {
+ command.current_dir(directory);
+ }
+
+ command.output().await.map_err(|e| anyhow!("{e}"))
+ };
+
let installation_path = self.install_if_needed().await?;
- let node_binary = installation_path.join("bin/node");
- let npm_file = installation_path.join("bin/npm");
-
- let output = Command::new(node_binary)
- .arg(npm_file)
- .arg(subcommand)
- .args(args)
- .current_dir(directory)
- .output()
- .await?;
+ let mut output = attempt(installation_path).await;
+ if output.is_err() {
+ let installation_path = self.reinstall().await?;
+ output = attempt(installation_path).await;
+ if output.is_err() {
+ return Err(anyhow!(
+ "failed to launch npm subcommand {subcommand} subcommand"
+ ));
+ }
+ }
- if !output.status.success() {
- return Err(anyhow!(
- "failed to execute npm {subcommand} subcommand:\nstdout: {:?}\nstderr: {:?}",
- String::from_utf8_lossy(&output.stdout),
- String::from_utf8_lossy(&output.stderr)
- ));
+ if let Ok(output) = &output {
+ if !output.status.success() {
+ return Err(anyhow!(
+ "failed to execute npm {subcommand} subcommand:\nstdout: {:?}\nstderr: {:?}",
+ String::from_utf8_lossy(&output.stdout),
+ String::from_utf8_lossy(&output.stderr)
+ ));
+ }
}
- Ok(())
+ output.map_err(|e| anyhow!("{e}"))
}
pub async fn npm_package_latest_version(&self, name: &str) -> Result<String> {
- let installation_path = self.install_if_needed().await?;
- let node_binary = installation_path.join("bin/node");
- let npm_file = installation_path.join("bin/npm");
-
- let output = Command::new(node_binary)
- .arg(npm_file)
- .args(["-fetch-retry-mintimeout", "2000"])
- .args(["-fetch-retry-maxtimeout", "5000"])
- .args(["-fetch-timeout", "5000"])
- .args(["info", name, "--json"])
- .output()
- .await
- .context("failed to run npm info")?;
-
- if !output.status.success() {
- return Err(anyhow!(
- "failed to execute npm info:\nstdout: {:?}\nstderr: {:?}",
- String::from_utf8_lossy(&output.stdout),
- String::from_utf8_lossy(&output.stderr)
- ));
- }
+ let output = self
+ .run_npm_subcommand(
+ None,
+ "info",
+ &[
+ name,
+ "--json",
+ "-fetch-retry-mintimeout",
+ "2000",
+ "-fetch-retry-maxtimeout",
+ "5000",
+ "-fetch-timeout",
+ "5000",
+ ],
+ )
+ .await?;
let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?;
info.dist_tags
@@ -112,41 +138,54 @@ impl NodeRuntime {
directory: &Path,
packages: impl IntoIterator<Item = (&str, &str)>,
) -> Result<()> {
- let installation_path = self.install_if_needed().await?;
- let node_binary = installation_path.join("bin/node");
- let npm_file = installation_path.join("bin/npm");
-
- let output = Command::new(node_binary)
- .arg(npm_file)
- .args(["-fetch-retry-mintimeout", "2000"])
- .args(["-fetch-retry-maxtimeout", "5000"])
- .args(["-fetch-timeout", "5000"])
- .arg("install")
- .arg("--prefix")
- .arg(directory)
- .args(
- packages
- .into_iter()
- .map(|(name, version)| format!("{name}@{version}")),
- )
- .output()
- .await
- .context("failed to run npm install")?;
-
- if !output.status.success() {
- return Err(anyhow!(
- "failed to execute npm install:\nstdout: {:?}\nstderr: {:?}",
- String::from_utf8_lossy(&output.stdout),
- String::from_utf8_lossy(&output.stderr)
- ));
- }
+ let packages: Vec<_> = packages
+ .into_iter()
+ .map(|(name, version)| format!("{name}@{version}"))
+ .collect();
+
+ let mut arguments: Vec<_> = packages.iter().map(|p| p.as_str()).collect();
+ arguments.extend_from_slice(&[
+ "-fetch-retry-mintimeout",
+ "2000",
+ "-fetch-retry-maxtimeout",
+ "5000",
+ "-fetch-timeout",
+ "5000",
+ ]);
+
+ self.run_npm_subcommand(Some(directory), "install", &arguments)
+ .await?;
Ok(())
}
+ async fn reinstall(&self) -> Result<PathBuf> {
+ log::info!("beginnning to reinstall Node runtime");
+ let mut installation_path = self.installation_path.lock().await;
+
+ if let Some(task) = installation_path.as_ref().cloned() {
+ if let Ok(installation_path) = task.await {
+ smol::fs::remove_dir_all(&installation_path)
+ .await
+ .context("node dir removal")
+ .log_err();
+ }
+ }
+
+ let http = self.http.clone();
+ let task = self
+ .background
+ .spawn(async move { Self::install(http).await.map_err(Arc::new) })
+ .shared();
+
+ *installation_path = Some(task.clone());
+ task.await.map_err(|e| anyhow!("{}", e))
+ }
+
async fn install_if_needed(&self) -> Result<PathBuf> {
let task = self
.installation_path
.lock()
+ .await
.get_or_insert_with(|| {
let http = self.http.clone();
self.background
@@ -155,13 +194,11 @@ impl NodeRuntime {
})
.clone();
- match task.await {
- Ok(path) => Ok(path),
- Err(error) => Err(anyhow!("{}", error)),
- }
+ task.await.map_err(|e| anyhow!("{}", e))
}
async fn install(http: Arc<dyn HttpClient>) -> Result<PathBuf> {
+ log::info!("installing Node runtime");
let arch = match consts::ARCH {
"x86_64" => "x64",
"aarch64" => "arm64",
@@ -45,7 +45,7 @@ use language::{
use log::error;
use lsp::{
DiagnosticSeverity, DiagnosticTag, DidChangeWatchedFilesRegistrationOptions,
- DocumentHighlightKind, LanguageServer, LanguageServerId,
+ DocumentHighlightKind, LanguageServer, LanguageServerBinary, LanguageServerId, OneOf,
};
use lsp_command::*;
use postage::watch;
@@ -65,6 +65,7 @@ use std::{
num::NonZeroU32,
ops::Range,
path::{self, Component, Path, PathBuf},
+ process::Stdio,
rc::Rc,
str,
sync::{
@@ -223,6 +224,7 @@ enum OpenBuffer {
Operations(Vec<Operation>),
}
+#[derive(Clone)]
enum WorktreeHandle {
Strong(ModelHandle<Worktree>),
Weak(WeakModelHandle<Worktree>),
@@ -279,6 +281,7 @@ pub enum Event {
pub enum LanguageServerState {
Starting(Task<Option<Arc<LanguageServer>>>),
+
Running {
language: Arc<Language>,
adapter: Arc<CachedLspAdapter>,
@@ -2424,348 +2427,505 @@ impl Project {
language: Arc<Language>,
cx: &mut ModelContext<Self>,
) {
- if !language_settings(
- Some(&language),
- worktree
- .update(cx, |tree, cx| tree.root_file(cx))
- .map(|f| f as _)
- .as_ref(),
- cx,
- )
- .enable_language_server
- {
+ let root_file = worktree.update(cx, |tree, cx| tree.root_file(cx));
+ let settings = language_settings(Some(&language), root_file.map(|f| f as _).as_ref(), cx);
+ if !settings.enable_language_server {
return;
}
let worktree_id = worktree.read(cx).id();
for adapter in language.lsp_adapters() {
- let key = (worktree_id, adapter.name.clone());
- if self.language_server_ids.contains_key(&key) {
- continue;
- }
-
- let pending_server = match self.languages.start_language_server(
- language.clone(),
- adapter.clone(),
+ self.start_language_server(
+ worktree_id,
worktree_path.clone(),
- ProjectLspAdapterDelegate::new(self, cx),
- cx,
- ) {
- Some(pending_server) => pending_server,
- None => continue,
- };
-
- let lsp = settings::get::<ProjectSettings>(cx)
- .lsp
- .get(&adapter.name.0);
- let override_options = lsp.map(|s| s.initialization_options.clone()).flatten();
-
- let mut initialization_options = adapter.initialization_options.clone();
- match (&mut initialization_options, override_options) {
- (Some(initialization_options), Some(override_options)) => {
- merge_json_value_into(override_options, initialization_options);
- }
- (None, override_options) => initialization_options = override_options,
- _ => {}
- }
-
- let server_id = pending_server.server_id;
- let state = self.setup_pending_language_server(
- initialization_options,
- pending_server,
adapter.clone(),
language.clone(),
- key.clone(),
cx,
);
- self.language_servers.insert(server_id, state);
- self.language_server_ids.insert(key.clone(), server_id);
}
}
- fn setup_pending_language_server(
+ fn start_language_server(
&mut self,
- initialization_options: Option<serde_json::Value>,
- pending_server: PendingLanguageServer,
+ worktree_id: WorktreeId,
+ worktree_path: Arc<Path>,
adapter: Arc<CachedLspAdapter>,
language: Arc<Language>,
- key: (WorktreeId, LanguageServerName),
- cx: &mut ModelContext<Project>,
- ) -> LanguageServerState {
+ cx: &mut ModelContext<Self>,
+ ) {
+ let key = (worktree_id, adapter.name.clone());
+ if self.language_server_ids.contains_key(&key) {
+ return;
+ }
+
+ let pending_server = match self.languages.create_pending_language_server(
+ language.clone(),
+ adapter.clone(),
+ worktree_path,
+ ProjectLspAdapterDelegate::new(self, cx),
+ cx,
+ ) {
+ Some(pending_server) => pending_server,
+ None => return,
+ };
+
+ let project_settings = settings::get::<ProjectSettings>(cx);
+ let lsp = project_settings.lsp.get(&adapter.name.0);
+ let override_options = lsp.map(|s| s.initialization_options.clone()).flatten();
+
+ let mut initialization_options = adapter.initialization_options.clone();
+ match (&mut initialization_options, override_options) {
+ (Some(initialization_options), Some(override_options)) => {
+ merge_json_value_into(override_options, initialization_options);
+ }
+ (None, override_options) => initialization_options = override_options,
+ _ => {}
+ }
+
let server_id = pending_server.server_id;
- let languages = self.languages.clone();
+ let container_dir = pending_server.container_dir.clone();
+ let state = LanguageServerState::Starting({
+ let adapter = adapter.clone();
+ let server_name = adapter.name.0.clone();
+ let languages = self.languages.clone();
+ let language = language.clone();
+ let key = key.clone();
+
+ cx.spawn_weak(|this, mut cx| async move {
+ let result = Self::setup_and_insert_language_server(
+ this,
+ initialization_options,
+ pending_server,
+ adapter.clone(),
+ languages,
+ language.clone(),
+ server_id,
+ key,
+ &mut cx,
+ )
+ .await;
- LanguageServerState::Starting(cx.spawn_weak(|this, mut cx| async move {
- let workspace_config = cx.update(|cx| languages.workspace_configuration(cx)).await;
- let language_server = pending_server.task.await.log_err()?;
+ match result {
+ Ok(server) => server,
+
+ Err(err) => {
+ log::error!("failed to start language server {:?}: {}", server_name, err);
- language_server
- .on_notification::<lsp::notification::LogMessage, _>({
- move |params, mut cx| {
if let Some(this) = this.upgrade(&cx) {
- this.update(&mut cx, |_, cx| {
- cx.emit(Event::LanguageServerLog(server_id, params.message))
- });
- }
- }
- })
- .detach();
+ if let Some(container_dir) = container_dir {
+ let installation_test_binary = adapter
+ .installation_test_binary(container_dir.to_path_buf())
+ .await;
- language_server
- .on_notification::<lsp::notification::PublishDiagnostics, _>({
- let adapter = adapter.clone();
- move |mut params, cx| {
- let adapter = adapter.clone();
- cx.spawn(|mut cx| async move {
- adapter.process_diagnostics(&mut params).await;
- if let Some(this) = this.upgrade(&cx) {
- this.update(&mut cx, |this, cx| {
- this.update_diagnostics(
+ this.update(&mut cx, |_, cx| {
+ Self::check_errored_server(
+ language,
+ adapter,
server_id,
- params,
- &adapter.disk_based_diagnostic_sources,
+ installation_test_binary,
cx,
)
- .log_err();
});
}
- })
- .detach();
- }
- })
- .detach();
-
- language_server
- .on_request::<lsp::request::WorkspaceConfiguration, _, _>({
- let languages = languages.clone();
- move |params, mut cx| {
- let languages = languages.clone();
- async move {
- let workspace_config =
- cx.update(|cx| languages.workspace_configuration(cx)).await;
- Ok(params
- .items
- .into_iter()
- .map(|item| {
- if let Some(section) = &item.section {
- workspace_config
- .get(section)
- .cloned()
- .unwrap_or(serde_json::Value::Null)
- } else {
- workspace_config.clone()
- }
- })
- .collect())
}
+
+ None
}
- })
- .detach();
+ }
+ })
+ });
- // Even though we don't have handling for these requests, respond to them to
- // avoid stalling any language server like `gopls` which waits for a response
- // to these requests when initializing.
- language_server
- .on_request::<lsp::request::WorkDoneProgressCreate, _, _>(
- move |params, mut cx| async move {
- if let Some(this) = this.upgrade(&cx) {
- this.update(&mut cx, |this, _| {
- if let Some(status) =
- this.language_server_statuses.get_mut(&server_id)
- {
- if let lsp::NumberOrString::String(token) = params.token {
- status.progress_tokens.insert(token);
- }
- }
- });
- }
- Ok(())
- },
- )
- .detach();
- language_server
- .on_request::<lsp::request::RegisterCapability, _, _>(
- move |params, mut cx| async move {
- let this = this
- .upgrade(&cx)
- .ok_or_else(|| anyhow!("project dropped"))?;
- for reg in params.registrations {
- if reg.method == "workspace/didChangeWatchedFiles" {
- if let Some(options) = reg.register_options {
- let options = serde_json::from_value(options)?;
- this.update(&mut cx, |this, cx| {
- this.on_lsp_did_change_watched_files(
- server_id, options, cx,
- );
- });
- }
- }
- }
- Ok(())
- },
- )
- .detach();
+ self.language_servers.insert(server_id, state);
+ self.language_server_ids.insert(key, server_id);
+ }
- language_server
- .on_request::<lsp::request::ApplyWorkspaceEdit, _, _>({
- let adapter = adapter.clone();
- move |params, cx| {
- Self::on_lsp_workspace_edit(this, params, server_id, adapter.clone(), cx)
- }
- })
- .detach();
+ fn reinstall_language_server(
+ &mut self,
+ language: Arc<Language>,
+ adapter: Arc<CachedLspAdapter>,
+ server_id: LanguageServerId,
+ cx: &mut ModelContext<Self>,
+ ) -> Option<Task<()>> {
+ log::info!("beginning to reinstall server");
- let disk_based_diagnostics_progress_token =
- adapter.disk_based_diagnostics_progress_token.clone();
+ let existing_server = match self.language_servers.remove(&server_id) {
+ Some(LanguageServerState::Running { server, .. }) => Some(server),
+ _ => None,
+ };
- language_server
- .on_notification::<lsp::notification::Progress, _>({
- move |params, mut cx| {
+ for worktree in &self.worktrees {
+ if let Some(worktree) = worktree.upgrade(cx) {
+ let key = (worktree.read(cx).id(), adapter.name.clone());
+ self.language_server_ids.remove(&key);
+ }
+ }
+
+ Some(cx.spawn(move |this, mut cx| async move {
+ if let Some(task) = existing_server.and_then(|server| server.shutdown()) {
+ log::info!("shutting down existing server");
+ task.await;
+ }
+
+ // TODO: This is race-safe with regards to preventing new instances from
+ // starting while deleting, but existing instances in other projects are going
+ // to be very confused and messed up
+ this.update(&mut cx, |this, cx| {
+ this.languages.delete_server_container(adapter.clone(), cx)
+ })
+ .await;
+
+ this.update(&mut cx, |this, mut cx| {
+ let worktrees = this.worktrees.clone();
+ for worktree in worktrees {
+ let worktree = match worktree.upgrade(cx) {
+ Some(worktree) => worktree.read(cx),
+ None => continue,
+ };
+ let worktree_id = worktree.id();
+ let root_path = worktree.abs_path();
+
+ this.start_language_server(
+ worktree_id,
+ root_path,
+ adapter.clone(),
+ language.clone(),
+ &mut cx,
+ );
+ }
+ })
+ }))
+ }
+
+ async fn setup_and_insert_language_server(
+ this: WeakModelHandle<Self>,
+ initialization_options: Option<serde_json::Value>,
+ pending_server: PendingLanguageServer,
+ adapter: Arc<CachedLspAdapter>,
+ languages: Arc<LanguageRegistry>,
+ language: Arc<Language>,
+ server_id: LanguageServerId,
+ key: (WorktreeId, LanguageServerName),
+ cx: &mut AsyncAppContext,
+ ) -> Result<Option<Arc<LanguageServer>>> {
+ let setup = Self::setup_pending_language_server(
+ this,
+ initialization_options,
+ pending_server,
+ adapter.clone(),
+ languages,
+ server_id,
+ cx,
+ );
+
+ let language_server = match setup.await? {
+ Some(language_server) => language_server,
+ None => return Ok(None),
+ };
+
+ let this = match this.upgrade(cx) {
+ Some(this) => this,
+ None => return Err(anyhow!("failed to upgrade project handle")),
+ };
+
+ this.update(cx, |this, cx| {
+ this.insert_newly_running_language_server(
+ language,
+ adapter,
+ language_server.clone(),
+ server_id,
+ key,
+ cx,
+ )
+ })?;
+
+ Ok(Some(language_server))
+ }
+
+ async fn setup_pending_language_server(
+ this: WeakModelHandle<Self>,
+ initialization_options: Option<serde_json::Value>,
+ pending_server: PendingLanguageServer,
+ adapter: Arc<CachedLspAdapter>,
+ languages: Arc<LanguageRegistry>,
+ server_id: LanguageServerId,
+ cx: &mut AsyncAppContext,
+ ) -> Result<Option<Arc<LanguageServer>>> {
+ let workspace_config = cx.update(|cx| languages.workspace_configuration(cx)).await;
+
+ let language_server = match pending_server.task.await? {
+ Some(server) => server.initialize(initialization_options).await?,
+ None => return Ok(None),
+ };
+
+ language_server
+ .on_notification::<lsp::notification::LogMessage, _>({
+ move |params, mut cx| {
+ if let Some(this) = this.upgrade(&cx) {
+ this.update(&mut cx, |_, cx| {
+ cx.emit(Event::LanguageServerLog(server_id, params.message))
+ });
+ }
+ }
+ })
+ .detach();
+
+ language_server
+ .on_notification::<lsp::notification::PublishDiagnostics, _>({
+ let adapter = adapter.clone();
+ move |mut params, cx| {
+ let this = this;
+ let adapter = adapter.clone();
+ cx.spawn(|mut cx| async move {
+ adapter.process_diagnostics(&mut params).await;
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| {
- this.on_lsp_progress(
- params,
+ this.update_diagnostics(
server_id,
- disk_based_diagnostics_progress_token.clone(),
+ params,
+ &adapter.disk_based_diagnostic_sources,
cx,
- );
+ )
+ .log_err();
});
}
+ })
+ .detach();
+ }
+ })
+ .detach();
+
+ language_server
+ .on_request::<lsp::request::WorkspaceConfiguration, _, _>({
+ let languages = languages.clone();
+ move |params, mut cx| {
+ let languages = languages.clone();
+ async move {
+ let workspace_config =
+ cx.update(|cx| languages.workspace_configuration(cx)).await;
+ Ok(params
+ .items
+ .into_iter()
+ .map(|item| {
+ if let Some(section) = &item.section {
+ workspace_config
+ .get(section)
+ .cloned()
+ .unwrap_or(serde_json::Value::Null)
+ } else {
+ workspace_config.clone()
+ }
+ })
+ .collect())
}
- })
- .detach();
+ }
+ })
+ .detach();
- let language_server = language_server
- .initialize(initialization_options)
- .await
- .log_err()?;
- language_server
- .notify::<lsp::notification::DidChangeConfiguration>(
- lsp::DidChangeConfigurationParams {
- settings: workspace_config,
- },
- )
- .ok();
+ // Even though we don't have handling for these requests, respond to them to
+ // avoid stalling any language server like `gopls` which waits for a response
+ // to these requests when initializing.
+ language_server
+ .on_request::<lsp::request::WorkDoneProgressCreate, _, _>(
+ move |params, mut cx| async move {
+ if let Some(this) = this.upgrade(&cx) {
+ this.update(&mut cx, |this, _| {
+ if let Some(status) = this.language_server_statuses.get_mut(&server_id)
+ {
+ if let lsp::NumberOrString::String(token) = params.token {
+ status.progress_tokens.insert(token);
+ }
+ }
+ });
+ }
+ Ok(())
+ },
+ )
+ .detach();
+ language_server
+ .on_request::<lsp::request::RegisterCapability, _, _>({
+ move |params, mut cx| async move {
+ let this = this
+ .upgrade(&cx)
+ .ok_or_else(|| anyhow!("project dropped"))?;
+ for reg in params.registrations {
+ if reg.method == "workspace/didChangeWatchedFiles" {
+ if let Some(options) = reg.register_options {
+ let options = serde_json::from_value(options)?;
+ this.update(&mut cx, |this, cx| {
+ this.on_lsp_did_change_watched_files(server_id, options, cx);
+ });
+ }
+ }
+ }
+ Ok(())
+ }
+ })
+ .detach();
- let this = this.upgrade(&cx)?;
- this.update(&mut cx, |this, cx| {
- // If the language server for this key doesn't match the server id, don't store the
- // server. Which will cause it to be dropped, killing the process
- if this
- .language_server_ids
- .get(&key)
- .map(|id| id != &server_id)
- .unwrap_or(false)
- {
- return None;
+ language_server
+ .on_request::<lsp::request::ApplyWorkspaceEdit, _, _>({
+ let adapter = adapter.clone();
+ move |params, cx| {
+ Self::on_lsp_workspace_edit(this, params, server_id, adapter.clone(), cx)
}
+ })
+ .detach();
- // Update language_servers collection with Running variant of LanguageServerState
- // indicating that the server is up and running and ready
- this.language_servers.insert(
- server_id,
- LanguageServerState::Running {
- adapter: adapter.clone(),
- language: language.clone(),
- watched_paths: Default::default(),
- server: language_server.clone(),
- simulate_disk_based_diagnostics_completion: None,
- },
- );
- this.language_server_statuses.insert(
- server_id,
- LanguageServerStatus {
- name: language_server.name().to_string(),
- pending_work: Default::default(),
- has_pending_diagnostic_updates: false,
- progress_tokens: Default::default(),
- },
- );
+ let disk_based_diagnostics_progress_token =
+ adapter.disk_based_diagnostics_progress_token.clone();
- cx.emit(Event::LanguageServerAdded(server_id));
+ language_server
+ .on_notification::<lsp::notification::Progress, _>(move |params, mut cx| {
+ if let Some(this) = this.upgrade(&cx) {
+ this.update(&mut cx, |this, cx| {
+ this.on_lsp_progress(
+ params,
+ server_id,
+ disk_based_diagnostics_progress_token.clone(),
+ cx,
+ );
+ });
+ }
+ })
+ .detach();
- if let Some(project_id) = this.remote_id() {
- this.client
- .send(proto::StartLanguageServer {
- project_id,
- server: Some(proto::LanguageServer {
- id: server_id.0 as u64,
- name: language_server.name().to_string(),
- }),
- })
- .log_err();
+ language_server
+ .notify::<lsp::notification::DidChangeConfiguration>(
+ lsp::DidChangeConfigurationParams {
+ settings: workspace_config,
+ },
+ )
+ .ok();
+
+ Ok(Some(language_server))
+ }
+
+ fn insert_newly_running_language_server(
+ &mut self,
+ language: Arc<Language>,
+ adapter: Arc<CachedLspAdapter>,
+ language_server: Arc<LanguageServer>,
+ server_id: LanguageServerId,
+ key: (WorktreeId, LanguageServerName),
+ cx: &mut ModelContext<Self>,
+ ) -> Result<()> {
+ // If the language server for this key doesn't match the server id, don't store the
+ // server. Which will cause it to be dropped, killing the process
+ if self
+ .language_server_ids
+ .get(&key)
+ .map(|id| id != &server_id)
+ .unwrap_or(false)
+ {
+ return Ok(());
+ }
+
+ // Update language_servers collection with Running variant of LanguageServerState
+ // indicating that the server is up and running and ready
+ self.language_servers.insert(
+ server_id,
+ LanguageServerState::Running {
+ adapter: adapter.clone(),
+ language: language.clone(),
+ watched_paths: Default::default(),
+ server: language_server.clone(),
+ simulate_disk_based_diagnostics_completion: None,
+ },
+ );
+
+ self.language_server_statuses.insert(
+ server_id,
+ LanguageServerStatus {
+ name: language_server.name().to_string(),
+ pending_work: Default::default(),
+ has_pending_diagnostic_updates: false,
+ progress_tokens: Default::default(),
+ },
+ );
+
+ cx.emit(Event::LanguageServerAdded(server_id));
+
+ if let Some(project_id) = self.remote_id() {
+ self.client.send(proto::StartLanguageServer {
+ project_id,
+ server: Some(proto::LanguageServer {
+ id: server_id.0 as u64,
+ name: language_server.name().to_string(),
+ }),
+ })?;
+ }
+
+ // Tell the language server about every open buffer in the worktree that matches the language.
+ for buffer in self.opened_buffers.values() {
+ if let Some(buffer_handle) = buffer.upgrade(cx) {
+ let buffer = buffer_handle.read(cx);
+ let file = match File::from_dyn(buffer.file()) {
+ Some(file) => file,
+ None => continue,
+ };
+ let language = match buffer.language() {
+ Some(language) => language,
+ None => continue,
+ };
+
+ if file.worktree.read(cx).id() != key.0
+ || !language.lsp_adapters().iter().any(|a| a.name == key.1)
+ {
+ continue;
}
- // Tell the language server about every open buffer in the worktree that matches the language.
- for buffer in this.opened_buffers.values() {
- if let Some(buffer_handle) = buffer.upgrade(cx) {
- let buffer = buffer_handle.read(cx);
- let file = match File::from_dyn(buffer.file()) {
- Some(file) => file,
- None => continue,
- };
- let language = match buffer.language() {
- Some(language) => language,
- None => continue,
- };
+ let file = match file.as_local() {
+ Some(file) => file,
+ None => continue,
+ };
- if file.worktree.read(cx).id() != key.0
- || !language.lsp_adapters().iter().any(|a| a.name == key.1)
- {
- continue;
- }
+ let versions = self
+ .buffer_snapshots
+ .entry(buffer.remote_id())
+ .or_default()
+ .entry(server_id)
+ .or_insert_with(|| {
+ vec![LspBufferSnapshot {
+ version: 0,
+ snapshot: buffer.text_snapshot(),
+ }]
+ });
- let file = file.as_local()?;
- let versions = this
- .buffer_snapshots
- .entry(buffer.remote_id())
- .or_default()
- .entry(server_id)
- .or_insert_with(|| {
- vec![LspBufferSnapshot {
- version: 0,
- snapshot: buffer.text_snapshot(),
- }]
- });
+ let snapshot = versions.last().unwrap();
+ let version = snapshot.version;
+ let initial_snapshot = &snapshot.snapshot;
+ let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap();
+ language_server.notify::<lsp::notification::DidOpenTextDocument>(
+ lsp::DidOpenTextDocumentParams {
+ text_document: lsp::TextDocumentItem::new(
+ uri,
+ adapter
+ .language_ids
+ .get(language.name().as_ref())
+ .cloned()
+ .unwrap_or_default(),
+ version,
+ initial_snapshot.text(),
+ ),
+ },
+ )?;
- let snapshot = versions.last().unwrap();
- let version = snapshot.version;
- let initial_snapshot = &snapshot.snapshot;
- let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap();
+ buffer_handle.update(cx, |buffer, cx| {
+ buffer.set_completion_triggers(
language_server
- .notify::<lsp::notification::DidOpenTextDocument>(
- lsp::DidOpenTextDocumentParams {
- text_document: lsp::TextDocumentItem::new(
- uri,
- adapter
- .language_ids
- .get(language.name().as_ref())
- .cloned()
- .unwrap_or_default(),
- version,
- initial_snapshot.text(),
- ),
- },
- )
- .log_err()?;
- buffer_handle.update(cx, |buffer, cx| {
- buffer.set_completion_triggers(
- language_server
- .capabilities()
- .completion_provider
- .as_ref()
- .and_then(|provider| provider.trigger_characters.clone())
- .unwrap_or_default(),
- cx,
- )
- });
- }
- }
+ .capabilities()
+ .completion_provider
+ .as_ref()
+ .and_then(|provider| provider.trigger_characters.clone())
+ .unwrap_or_default(),
+ cx,
+ )
+ });
+ }
+ }
- cx.notify();
- Some(language_server)
- })
- }))
+ cx.notify();
+ Ok(())
}
// Returns a list of all of the worktrees which no longer have a language server and the root path
@@ -2814,9 +2974,7 @@ impl Project {
let mut root_path = None;
let server = match server_state {
- Some(LanguageServerState::Starting(started_language_server)) => {
- started_language_server.await
- }
+ Some(LanguageServerState::Starting(task)) => task.await,
Some(LanguageServerState::Running { server, .. }) => Some(server),
None => None,
};
@@ -2927,6 +3085,72 @@ impl Project {
.detach();
}
+ fn check_errored_server(
+ language: Arc<Language>,
+ adapter: Arc<CachedLspAdapter>,
+ server_id: LanguageServerId,
+ installation_test_binary: Option<LanguageServerBinary>,
+ cx: &mut ModelContext<Self>,
+ ) {
+ if !adapter.can_be_reinstalled() {
+ log::info!(
+ "Validation check requested for {:?} but it cannot be reinstalled",
+ adapter.name.0
+ );
+ return;
+ }
+
+ cx.spawn(|this, mut cx| async move {
+ log::info!("About to spawn test binary");
+
+ // A lack of test binary counts as a failure
+ let process = installation_test_binary.and_then(|binary| {
+ smol::process::Command::new(&binary.path)
+ .current_dir(&binary.path)
+ .args(binary.arguments)
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .stderr(Stdio::inherit())
+ .kill_on_drop(true)
+ .spawn()
+ .ok()
+ });
+
+ const PROCESS_TIMEOUT: Duration = Duration::from_secs(5);
+ let mut timeout = cx.background().timer(PROCESS_TIMEOUT).fuse();
+
+ let mut errored = false;
+ if let Some(mut process) = process {
+ futures::select! {
+ status = process.status().fuse() => match status {
+ Ok(status) => errored = !status.success(),
+ Err(_) => errored = true,
+ },
+
+ _ = timeout => {
+ log::info!("test binary time-ed out, this counts as a success");
+ _ = process.kill();
+ }
+ }
+ } else {
+ log::warn!("test binary failed to launch");
+ errored = true;
+ }
+
+ if errored {
+ log::warn!("test binary check failed");
+ let task = this.update(&mut cx, move |this, mut cx| {
+ this.reinstall_language_server(language, adapter, server_id, &mut cx)
+ });
+
+ if let Some(task) = task {
+ task.await;
+ }
+ }
+ })
+ .detach();
+ }
+
fn on_lsp_progress(
&mut self,
progress: lsp::ProgressParams,
@@ -3719,14 +3943,15 @@ impl Project {
tab_size: NonZeroU32,
cx: &mut AsyncAppContext,
) -> Result<Vec<(Range<Anchor>, String)>> {
- let text_document =
- lsp::TextDocumentIdentifier::new(lsp::Url::from_file_path(abs_path).unwrap());
+ let uri = lsp::Url::from_file_path(abs_path)
+ .map_err(|_| anyhow!("failed to convert abs path to uri"))?;
+ let text_document = lsp::TextDocumentIdentifier::new(uri);
let capabilities = &language_server.capabilities();
- let lsp_edits = if capabilities
- .document_formatting_provider
- .as_ref()
- .map_or(false, |provider| *provider != lsp::OneOf::Left(false))
- {
+
+ let formatting_provider = capabilities.document_formatting_provider.as_ref();
+ let range_formatting_provider = capabilities.document_range_formatting_provider.as_ref();
+
+ let lsp_edits = if matches!(formatting_provider, Some(p) if *p != OneOf::Left(false)) {
language_server
.request::<lsp::request::Formatting>(lsp::DocumentFormattingParams {
text_document,
@@ -3734,14 +3959,10 @@ impl Project {
work_done_progress_params: Default::default(),
})
.await?
- } else if capabilities
- .document_range_formatting_provider
- .as_ref()
- .map_or(false, |provider| *provider != lsp::OneOf::Left(false))
- {
+ } else if matches!(range_formatting_provider, Some(p) if *p != OneOf::Left(false)) {
let buffer_start = lsp::Position::new(0, 0);
- let buffer_end =
- buffer.read_with(cx, |buffer, _| point_to_lsp(buffer.max_point_utf16()));
+ let buffer_end = buffer.read_with(cx, |b, _| point_to_lsp(b.max_point_utf16()));
+
language_server
.request::<lsp::request::RangeFormatting>(lsp::DocumentRangeFormattingParams {
text_document,
@@ -3760,7 +3981,7 @@ impl Project {
})
.await
} else {
- Ok(Default::default())
+ Ok(Vec::new())
}
}
@@ -3868,70 +4089,72 @@ impl Project {
let mut requests = Vec::new();
for ((worktree_id, _), server_id) in self.language_server_ids.iter() {
let worktree_id = *worktree_id;
- if let Some(worktree) = self
- .worktree_for_id(worktree_id, cx)
- .and_then(|worktree| worktree.read(cx).as_local())
- {
- if let Some(LanguageServerState::Running {
+ let worktree_handle = self.worktree_for_id(worktree_id, cx);
+ let worktree = match worktree_handle.and_then(|tree| tree.read(cx).as_local()) {
+ Some(worktree) => worktree,
+ None => continue,
+ };
+ let worktree_abs_path = worktree.abs_path().clone();
+
+ let (adapter, language, server) = match self.language_servers.get(server_id) {
+ Some(LanguageServerState::Running {
adapter,
language,
server,
..
- }) = self.language_servers.get(server_id)
- {
- let adapter = adapter.clone();
- let language = language.clone();
- let worktree_abs_path = worktree.abs_path().clone();
- requests.push(
- server
- .request::<lsp::request::WorkspaceSymbolRequest>(
- lsp::WorkspaceSymbolParams {
- query: query.to_string(),
- ..Default::default()
- },
- )
- .log_err()
- .map(move |response| {
- let lsp_symbols = response.flatten().map(|symbol_response| match symbol_response {
- lsp::WorkspaceSymbolResponse::Flat(flat_responses) => {
- flat_responses.into_iter().map(|lsp_symbol| {
- (lsp_symbol.name, lsp_symbol.kind, lsp_symbol.location)
- }).collect::<Vec<_>>()
- }
- lsp::WorkspaceSymbolResponse::Nested(nested_responses) => {
- nested_responses.into_iter().filter_map(|lsp_symbol| {
- let location = match lsp_symbol.location {
- lsp::OneOf::Left(location) => location,
- lsp::OneOf::Right(_) => {
- error!("Unexpected: client capabilities forbid symbol resolutions in workspace.symbol.resolveSupport");
- return None
- }
- };
- Some((lsp_symbol.name, lsp_symbol.kind, location))
- }).collect::<Vec<_>>()
- }
- }).unwrap_or_default();
+ }) => (adapter.clone(), language.clone(), server),
- (
- adapter,
- language,
- worktree_id,
- worktree_abs_path,
- lsp_symbols,
- )
- }),
- );
- }
- }
+ _ => continue,
+ };
+
+ requests.push(
+ server
+ .request::<lsp::request::WorkspaceSymbolRequest>(
+ lsp::WorkspaceSymbolParams {
+ query: query.to_string(),
+ ..Default::default()
+ },
+ )
+ .log_err()
+ .map(move |response| {
+ let lsp_symbols = response.flatten().map(|symbol_response| match symbol_response {
+ lsp::WorkspaceSymbolResponse::Flat(flat_responses) => {
+ flat_responses.into_iter().map(|lsp_symbol| {
+ (lsp_symbol.name, lsp_symbol.kind, lsp_symbol.location)
+ }).collect::<Vec<_>>()
+ }
+ lsp::WorkspaceSymbolResponse::Nested(nested_responses) => {
+ nested_responses.into_iter().filter_map(|lsp_symbol| {
+ let location = match lsp_symbol.location {
+ OneOf::Left(location) => location,
+ OneOf::Right(_) => {
+ error!("Unexpected: client capabilities forbid symbol resolutions in workspace.symbol.resolveSupport");
+ return None
+ }
+ };
+ Some((lsp_symbol.name, lsp_symbol.kind, location))
+ }).collect::<Vec<_>>()
+ }
+ }).unwrap_or_default();
+
+ (
+ adapter,
+ language,
+ worktree_id,
+ worktree_abs_path,
+ lsp_symbols,
+ )
+ }),
+ );
}
cx.spawn_weak(|this, cx| async move {
let responses = futures::future::join_all(requests).await;
- let this = if let Some(this) = this.upgrade(&cx) {
- this
- } else {
- return Ok(Default::default());
+ let this = match this.upgrade(&cx) {
+ Some(this) => this,
+ None => return Ok(Vec::new()),
};
+
let symbols = this.read_with(&cx, |this, cx| {
let mut symbols = Vec::new();
for (
@@ -118,14 +118,15 @@ pub fn merge_non_null_json_value_into(source: serde_json::Value, target: &mut se
}
}
-pub trait ResultExt {
+pub trait ResultExt<E> {
type Ok;
fn log_err(self) -> Option<Self::Ok>;
fn warn_on_err(self) -> Option<Self::Ok>;
+ fn inspect_error(self, func: impl FnOnce(&E)) -> Self;
}
-impl<T, E> ResultExt for Result<T, E>
+impl<T, E> ResultExt<E> for Result<T, E>
where
E: std::fmt::Debug,
{
@@ -152,6 +153,15 @@ where
}
}
}
+
+ /// https://doc.rust-lang.org/std/result/enum.Result.html#method.inspect_err
+ fn inspect_error(self, func: impl FnOnce(&E)) -> Self {
+ if let Err(err) = &self {
+ func(err);
+ }
+
+ self
+ }
}
pub trait TryFutureExt {
@@ -2,6 +2,7 @@ use anyhow::{anyhow, Context, Result};
use async_trait::async_trait;
use futures::StreamExt;
pub use language::*;
+use lsp::LanguageServerBinary;
use smol::fs::{self, File};
use std::{any::Any, path::PathBuf, sync::Arc};
use util::{
@@ -86,31 +87,19 @@ impl super::LspAdapter for CLspAdapter {
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
- (|| async move {
- let mut last_clangd_dir = None;
- let mut entries = fs::read_dir(&container_dir).await?;
- while let Some(entry) = entries.next().await {
- let entry = entry?;
- if entry.file_type().await?.is_dir() {
- last_clangd_dir = Some(entry.path());
- }
- }
- let clangd_dir = last_clangd_dir.ok_or_else(|| anyhow!("no cached binary"))?;
- let clangd_bin = clangd_dir.join("bin/clangd");
- if clangd_bin.exists() {
- Ok(LanguageServerBinary {
- path: clangd_bin,
- arguments: vec![],
- })
- } else {
- Err(anyhow!(
- "missing clangd binary in directory {:?}",
- clangd_dir
- ))
- }
- })()
- .await
- .log_err()
+ get_cached_server_binary(container_dir).await
+ }
+
+ async fn installation_test_binary(
+ &self,
+ container_dir: PathBuf,
+ ) -> Option<LanguageServerBinary> {
+ get_cached_server_binary(container_dir)
+ .await
+ .map(|mut binary| {
+ binary.arguments = vec!["--help".into()];
+ binary
+ })
}
async fn label_for_completion(
@@ -250,6 +239,34 @@ impl super::LspAdapter for CLspAdapter {
}
}
+async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
+ (|| async move {
+ let mut last_clangd_dir = None;
+ let mut entries = fs::read_dir(&container_dir).await?;
+ while let Some(entry) = entries.next().await {
+ let entry = entry?;
+ if entry.file_type().await?.is_dir() {
+ last_clangd_dir = Some(entry.path());
+ }
+ }
+ let clangd_dir = last_clangd_dir.ok_or_else(|| anyhow!("no cached binary"))?;
+ let clangd_bin = clangd_dir.join("bin/clangd");
+ if clangd_bin.exists() {
+ Ok(LanguageServerBinary {
+ path: clangd_bin,
+ arguments: vec![],
+ })
+ } else {
+ Err(anyhow!(
+ "missing clangd binary in directory {:?}",
+ clangd_dir
+ ))
+ }
+ })()
+ .await
+ .log_err()
+}
+
#[cfg(test)]
mod tests {
use gpui::TestAppContext;
@@ -3,7 +3,7 @@ use async_trait::async_trait;
use futures::StreamExt;
use gpui::{AsyncAppContext, Task};
pub use language::*;
-use lsp::{CompletionItemKind, SymbolKind};
+use lsp::{CompletionItemKind, LanguageServerBinary, SymbolKind};
use smol::fs::{self, File};
use std::{
any::Any,
@@ -140,20 +140,14 @@ impl LspAdapter for ElixirLspAdapter {
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
- (|| async move {
- let mut last = None;
- let mut entries = fs::read_dir(&container_dir).await?;
- while let Some(entry) = entries.next().await {
- last = Some(entry?.path());
- }
- last.map(|path| LanguageServerBinary {
- path,
- arguments: vec![],
- })
- .ok_or_else(|| anyhow!("no cached binary"))
- })()
- .await
- .log_err()
+ get_cached_server_binary(container_dir).await
+ }
+
+ async fn installation_test_binary(
+ &self,
+ container_dir: PathBuf,
+ ) -> Option<LanguageServerBinary> {
+ get_cached_server_binary(container_dir).await
}
async fn label_for_completion(
@@ -239,3 +233,20 @@ impl LspAdapter for ElixirLspAdapter {
})
}
}
+
+async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
+ (|| async move {
+ let mut last = None;
+ let mut entries = fs::read_dir(&container_dir).await?;
+ while let Some(entry) = entries.next().await {
+ last = Some(entry?.path());
+ }
+ last.map(|path| LanguageServerBinary {
+ path,
+ arguments: vec![],
+ })
+ .ok_or_else(|| anyhow!("no cached binary"))
+ })()
+ .await
+ .log_err()
+}
@@ -4,6 +4,7 @@ use futures::StreamExt;
use gpui::{AsyncAppContext, Task};
pub use language::*;
use lazy_static::lazy_static;
+use lsp::LanguageServerBinary;
use regex::Regex;
use smol::{fs, process};
use std::{
@@ -148,32 +149,19 @@ impl super::LspAdapter for GoLspAdapter {
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
- (|| async move {
- let mut last_binary_path = None;
- let mut entries = fs::read_dir(&container_dir).await?;
- while let Some(entry) = entries.next().await {
- let entry = entry?;
- if entry.file_type().await?.is_file()
- && entry
- .file_name()
- .to_str()
- .map_or(false, |name| name.starts_with("gopls_"))
- {
- last_binary_path = Some(entry.path());
- }
- }
+ get_cached_server_binary(container_dir).await
+ }
- if let Some(path) = last_binary_path {
- Ok(LanguageServerBinary {
- path,
- arguments: server_binary_arguments(),
- })
- } else {
- Err(anyhow!("no cached binary"))
- }
- })()
- .await
- .log_err()
+ async fn installation_test_binary(
+ &self,
+ container_dir: PathBuf,
+ ) -> Option<LanguageServerBinary> {
+ get_cached_server_binary(container_dir)
+ .await
+ .map(|mut binary| {
+ binary.arguments = vec!["--help".into()];
+ binary
+ })
}
async fn label_for_completion(
@@ -336,6 +324,35 @@ impl super::LspAdapter for GoLspAdapter {
}
}
+async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
+ (|| async move {
+ let mut last_binary_path = None;
+ let mut entries = fs::read_dir(&container_dir).await?;
+ while let Some(entry) = entries.next().await {
+ let entry = entry?;
+ if entry.file_type().await?.is_file()
+ && entry
+ .file_name()
+ .to_str()
+ .map_or(false, |name| name.starts_with("gopls_"))
+ {
+ last_binary_path = Some(entry.path());
+ }
+ }
+
+ if let Some(path) = last_binary_path {
+ Ok(LanguageServerBinary {
+ path,
+ arguments: server_binary_arguments(),
+ })
+ } else {
+ Err(anyhow!("no cached binary"))
+ }
+ })()
+ .await
+ .log_err()
+}
+
fn adjust_runs(
delta: usize,
mut runs: Vec<(Range<usize>, HighlightId)>,
@@ -1,7 +1,8 @@
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use futures::StreamExt;
-use language::{LanguageServerBinary, LanguageServerName, LspAdapter, LspAdapterDelegate};
+use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp::LanguageServerBinary;
use node_runtime::NodeRuntime;
use serde_json::json;
use smol::fs;
@@ -13,6 +14,9 @@ use std::{
};
use util::ResultExt;
+const SERVER_PATH: &'static str =
+ "node_modules/vscode-langservers-extracted/bin/vscode-html-language-server";
+
fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
vec![server_path.into(), "--stdio".into()]
}
@@ -22,9 +26,6 @@ pub struct HtmlLspAdapter {
}
impl HtmlLspAdapter {
- const SERVER_PATH: &'static str =
- "node_modules/vscode-langservers-extracted/bin/vscode-html-language-server";
-
pub fn new(node: Arc<NodeRuntime>) -> Self {
HtmlLspAdapter { node }
}
@@ -54,7 +55,7 @@ impl LspAdapter for HtmlLspAdapter {
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let version = version.downcast::<String>().unwrap();
- let server_path = container_dir.join(Self::SERVER_PATH);
+ let server_path = container_dir.join(SERVER_PATH);
if fs::metadata(&server_path).await.is_err() {
self.node
@@ -76,31 +77,14 @@ impl LspAdapter for HtmlLspAdapter {
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
- (|| async move {
- let mut last_version_dir = None;
- let mut entries = fs::read_dir(&container_dir).await?;
- while let Some(entry) = entries.next().await {
- let entry = entry?;
- if entry.file_type().await?.is_dir() {
- last_version_dir = Some(entry.path());
- }
- }
- let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
- let server_path = last_version_dir.join(Self::SERVER_PATH);
- if server_path.exists() {
- Ok(LanguageServerBinary {
- path: self.node.binary_path().await?,
- arguments: server_binary_arguments(&server_path),
- })
- } else {
- Err(anyhow!(
- "missing executable in directory {:?}",
- last_version_dir
- ))
- }
- })()
- .await
- .log_err()
+ get_cached_server_binary(container_dir, &self.node).await
+ }
+
+ async fn installation_test_binary(
+ &self,
+ container_dir: PathBuf,
+ ) -> Option<LanguageServerBinary> {
+ get_cached_server_binary(container_dir, &self.node).await
}
async fn initialization_options(&self) -> Option<serde_json::Value> {
@@ -109,3 +93,34 @@ impl LspAdapter for HtmlLspAdapter {
}))
}
}
+
+async fn get_cached_server_binary(
+ container_dir: PathBuf,
+ node: &NodeRuntime,
+) -> Option<LanguageServerBinary> {
+ (|| async move {
+ let mut last_version_dir = None;
+ let mut entries = fs::read_dir(&container_dir).await?;
+ while let Some(entry) = entries.next().await {
+ let entry = entry?;
+ if entry.file_type().await?.is_dir() {
+ last_version_dir = Some(entry.path());
+ }
+ }
+ let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
+ let server_path = last_version_dir.join(SERVER_PATH);
+ if server_path.exists() {
+ Ok(LanguageServerBinary {
+ path: node.binary_path().await?,
+ arguments: server_binary_arguments(&server_path),
+ })
+ } else {
+ Err(anyhow!(
+ "missing executable in directory {:?}",
+ last_version_dir
+ ))
+ }
+ })()
+ .await
+ .log_err()
+}
@@ -3,9 +3,8 @@ use async_trait::async_trait;
use collections::HashMap;
use futures::{future::BoxFuture, FutureExt, StreamExt};
use gpui::AppContext;
-use language::{
- LanguageRegistry, LanguageServerBinary, LanguageServerName, LspAdapter, LspAdapterDelegate,
-};
+use language::{LanguageRegistry, LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp::LanguageServerBinary;
use node_runtime::NodeRuntime;
use serde_json::json;
use settings::{KeymapFile, SettingsJsonSchemaParams, SettingsStore};
@@ -84,32 +83,14 @@ impl LspAdapter for JsonLspAdapter {
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
- (|| async move {
- let mut last_version_dir = None;
- let mut entries = fs::read_dir(&container_dir).await?;
- while let Some(entry) = entries.next().await {
- let entry = entry?;
- if entry.file_type().await?.is_dir() {
- last_version_dir = Some(entry.path());
- }
- }
+ get_cached_server_binary(container_dir, &self.node).await
+ }
- let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
- let server_path = last_version_dir.join(SERVER_PATH);
- if server_path.exists() {
- Ok(LanguageServerBinary {
- path: self.node.binary_path().await?,
- arguments: server_binary_arguments(&server_path),
- })
- } else {
- Err(anyhow!(
- "missing executable in directory {:?}",
- last_version_dir
- ))
- }
- })()
- .await
- .log_err()
+ async fn installation_test_binary(
+ &self,
+ container_dir: PathBuf,
+ ) -> Option<LanguageServerBinary> {
+ get_cached_server_binary(container_dir, &self.node).await
}
async fn initialization_options(&self) -> Option<serde_json::Value> {
@@ -162,6 +143,38 @@ impl LspAdapter for JsonLspAdapter {
}
}
+async fn get_cached_server_binary(
+ container_dir: PathBuf,
+ node: &NodeRuntime,
+) -> Option<LanguageServerBinary> {
+ (|| async move {
+ let mut last_version_dir = None;
+ let mut entries = fs::read_dir(&container_dir).await?;
+ while let Some(entry) = entries.next().await {
+ let entry = entry?;
+ if entry.file_type().await?.is_dir() {
+ last_version_dir = Some(entry.path());
+ }
+ }
+
+ let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
+ let server_path = last_version_dir.join(SERVER_PATH);
+ if server_path.exists() {
+ Ok(LanguageServerBinary {
+ path: node.binary_path().await?,
+ arguments: server_binary_arguments(&server_path),
+ })
+ } else {
+ Err(anyhow!(
+ "missing executable in directory {:?}",
+ last_version_dir
+ ))
+ }
+ })()
+ .await
+ .log_err()
+}
+
fn schema_file_match(path: &Path) -> &Path {
path.strip_prefix(path.parent().unwrap().parent().unwrap())
.unwrap()
@@ -3,7 +3,8 @@ use async_trait::async_trait;
use collections::HashMap;
use futures::lock::Mutex;
use gpui::executor::Background;
-use language::{LanguageServerBinary, LanguageServerName, LspAdapter, LspAdapterDelegate};
+use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp::LanguageServerBinary;
use plugin_runtime::{Plugin, PluginBinary, PluginBuilder, WasiFn};
use std::{any::Any, path::PathBuf, sync::Arc};
use util::ResultExt;
@@ -129,6 +130,14 @@ impl LspAdapter for PluginLspAdapter {
.await
}
+ fn can_be_reinstalled(&self) -> bool {
+ false
+ }
+
+ async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
+ None
+ }
+
async fn initialization_options(&self) -> Option<serde_json::Value> {
let string: String = self
.runtime
@@ -3,7 +3,8 @@ use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use async_trait::async_trait;
use futures::{io::BufReader, StreamExt};
-use language::{LanguageServerBinary, LanguageServerName, LspAdapterDelegate};
+use language::{LanguageServerName, LspAdapterDelegate};
+use lsp::LanguageServerBinary;
use smol::fs;
use std::{any::Any, env::consts, ffi::OsString, path::PathBuf};
use util::{
@@ -91,31 +92,47 @@ impl super::LspAdapter for LuaLspAdapter {
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
- async_iife!({
- let mut last_binary_path = None;
- let mut entries = fs::read_dir(&container_dir).await?;
- while let Some(entry) = entries.next().await {
- let entry = entry?;
- if entry.file_type().await?.is_file()
- && entry
- .file_name()
- .to_str()
- .map_or(false, |name| name == "lua-language-server")
- {
- last_binary_path = Some(entry.path());
- }
- }
+ get_cached_server_binary(container_dir).await
+ }
- if let Some(path) = last_binary_path {
- Ok(LanguageServerBinary {
- path,
- arguments: server_binary_arguments(),
- })
- } else {
- Err(anyhow!("no cached binary"))
- }
- })
- .await
- .log_err()
+ async fn installation_test_binary(
+ &self,
+ container_dir: PathBuf,
+ ) -> Option<LanguageServerBinary> {
+ get_cached_server_binary(container_dir)
+ .await
+ .map(|mut binary| {
+ binary.arguments = vec!["--version".into()];
+ binary
+ })
}
}
+
+async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
+ async_iife!({
+ let mut last_binary_path = None;
+ let mut entries = fs::read_dir(&container_dir).await?;
+ while let Some(entry) = entries.next().await {
+ let entry = entry?;
+ if entry.file_type().await?.is_file()
+ && entry
+ .file_name()
+ .to_str()
+ .map_or(false, |name| name == "lua-language-server")
+ {
+ last_binary_path = Some(entry.path());
+ }
+ }
+
+ if let Some(path) = last_binary_path {
+ Ok(LanguageServerBinary {
+ path,
+ arguments: server_binary_arguments(),
+ })
+ } else {
+ Err(anyhow!("no cached binary"))
+ }
+ })
+ .await
+ .log_err()
+}
@@ -1,7 +1,8 @@
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use futures::StreamExt;
-use language::{LanguageServerBinary, LanguageServerName, LspAdapter, LspAdapterDelegate};
+use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp::LanguageServerBinary;
use node_runtime::NodeRuntime;
use smol::fs;
use std::{
@@ -12,6 +13,8 @@ use std::{
};
use util::ResultExt;
+const SERVER_PATH: &'static str = "node_modules/pyright/langserver.index.js";
+
fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
vec![server_path.into(), "--stdio".into()]
}
@@ -21,8 +24,6 @@ pub struct PythonLspAdapter {
}
impl PythonLspAdapter {
- const SERVER_PATH: &'static str = "node_modules/pyright/langserver.index.js";
-
pub fn new(node: Arc<NodeRuntime>) -> Self {
PythonLspAdapter { node }
}
@@ -48,7 +49,7 @@ impl LspAdapter for PythonLspAdapter {
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let version = version.downcast::<String>().unwrap();
- let server_path = container_dir.join(Self::SERVER_PATH);
+ let server_path = container_dir.join(SERVER_PATH);
if fs::metadata(&server_path).await.is_err() {
self.node
@@ -67,31 +68,14 @@ impl LspAdapter for PythonLspAdapter {
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
- (|| async move {
- let mut last_version_dir = None;
- let mut entries = fs::read_dir(&container_dir).await?;
- while let Some(entry) = entries.next().await {
- let entry = entry?;
- if entry.file_type().await?.is_dir() {
- last_version_dir = Some(entry.path());
- }
- }
- let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
- let server_path = last_version_dir.join(Self::SERVER_PATH);
- if server_path.exists() {
- Ok(LanguageServerBinary {
- path: self.node.binary_path().await?,
- arguments: server_binary_arguments(&server_path),
- })
- } else {
- Err(anyhow!(
- "missing executable in directory {:?}",
- last_version_dir
- ))
- }
- })()
- .await
- .log_err()
+ get_cached_server_binary(container_dir, &self.node).await
+ }
+
+ async fn installation_test_binary(
+ &self,
+ container_dir: PathBuf,
+ ) -> Option<LanguageServerBinary> {
+ get_cached_server_binary(container_dir, &self.node).await
}
async fn process_completion(&self, item: &mut lsp::CompletionItem) {
@@ -170,6 +154,37 @@ impl LspAdapter for PythonLspAdapter {
}
}
+async fn get_cached_server_binary(
+ container_dir: PathBuf,
+ node: &NodeRuntime,
+) -> Option<LanguageServerBinary> {
+ (|| async move {
+ let mut last_version_dir = None;
+ let mut entries = fs::read_dir(&container_dir).await?;
+ while let Some(entry) = entries.next().await {
+ let entry = entry?;
+ if entry.file_type().await?.is_dir() {
+ last_version_dir = Some(entry.path());
+ }
+ }
+ let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
+ let server_path = last_version_dir.join(SERVER_PATH);
+ if server_path.exists() {
+ Ok(LanguageServerBinary {
+ path: node.binary_path().await?,
+ arguments: server_binary_arguments(&server_path),
+ })
+ } else {
+ Err(anyhow!(
+ "missing executable in directory {:?}",
+ last_version_dir
+ ))
+ }
+ })()
+ .await
+ .log_err()
+}
+
#[cfg(test)]
mod tests {
use gpui::{ModelContext, TestAppContext};
@@ -1,6 +1,7 @@
use anyhow::{anyhow, Result};
use async_trait::async_trait;
-use language::{LanguageServerBinary, LanguageServerName, LspAdapter, LspAdapterDelegate};
+use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp::LanguageServerBinary;
use std::{any::Any, path::PathBuf, sync::Arc};
pub struct RubyLanguageServer;
@@ -38,6 +39,14 @@ impl LspAdapter for RubyLanguageServer {
})
}
+ fn can_be_reinstalled(&self) -> bool {
+ false
+ }
+
+ async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
+ None
+ }
+
async fn label_for_completion(
&self,
item: &lsp::CompletionItem,
@@ -4,6 +4,7 @@ use async_trait::async_trait;
use futures::{io::BufReader, StreamExt};
pub use language::*;
use lazy_static::lazy_static;
+use lsp::LanguageServerBinary;
use regex::Regex;
use smol::fs::{self, File};
use std::{any::Any, borrow::Cow, env::consts, path::PathBuf, str, sync::Arc};
@@ -78,20 +79,19 @@ impl LspAdapter for RustLspAdapter {
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
- (|| async move {
- let mut last = None;
- let mut entries = fs::read_dir(&container_dir).await?;
- while let Some(entry) = entries.next().await {
- last = Some(entry?.path());
- }
+ get_cached_server_binary(container_dir).await
+ }
- anyhow::Ok(LanguageServerBinary {
- path: last.ok_or_else(|| anyhow!("no cached binary"))?,
- arguments: Default::default(),
+ async fn installation_test_binary(
+ &self,
+ container_dir: PathBuf,
+ ) -> Option<LanguageServerBinary> {
+ get_cached_server_binary(container_dir)
+ .await
+ .map(|mut binary| {
+ binary.arguments = vec!["--help".into()];
+ binary
})
- })()
- .await
- .log_err()
}
async fn disk_based_diagnostic_sources(&self) -> Vec<String> {
@@ -258,6 +258,22 @@ impl LspAdapter for RustLspAdapter {
})
}
}
+async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
+ (|| async move {
+ let mut last = None;
+ let mut entries = fs::read_dir(&container_dir).await?;
+ while let Some(entry) = entries.next().await {
+ last = Some(entry?.path());
+ }
+
+ anyhow::Ok(LanguageServerBinary {
+ path: last.ok_or_else(|| anyhow!("no cached binary"))?,
+ arguments: Default::default(),
+ })
+ })()
+ .await
+ .log_err()
+}
#[cfg(test)]
mod tests {
@@ -4,8 +4,8 @@ use async_tar::Archive;
use async_trait::async_trait;
use futures::{future::BoxFuture, FutureExt};
use gpui::AppContext;
-use language::{LanguageServerBinary, LanguageServerName, LspAdapter, LspAdapterDelegate};
-use lsp::CodeActionKind;
+use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp::{CodeActionKind, LanguageServerBinary};
use node_runtime::NodeRuntime;
use serde_json::{json, Value};
use smol::{fs, io::BufReader, stream::StreamExt};
@@ -104,28 +104,14 @@ impl LspAdapter for TypeScriptLspAdapter {
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
- (|| async move {
- let old_server_path = container_dir.join(Self::OLD_SERVER_PATH);
- let new_server_path = container_dir.join(Self::NEW_SERVER_PATH);
- if new_server_path.exists() {
- Ok(LanguageServerBinary {
- path: self.node.binary_path().await?,
- arguments: typescript_server_binary_arguments(&new_server_path),
- })
- } else if old_server_path.exists() {
- Ok(LanguageServerBinary {
- path: self.node.binary_path().await?,
- arguments: typescript_server_binary_arguments(&old_server_path),
- })
- } else {
- Err(anyhow!(
- "missing executable in directory {:?}",
- container_dir
- ))
- }
- })()
- .await
- .log_err()
+ get_cached_ts_server_binary(container_dir, &self.node).await
+ }
+
+ async fn installation_test_binary(
+ &self,
+ container_dir: PathBuf,
+ ) -> Option<LanguageServerBinary> {
+ get_cached_ts_server_binary(container_dir, &self.node).await
}
fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
@@ -173,6 +159,34 @@ impl LspAdapter for TypeScriptLspAdapter {
}
}
+async fn get_cached_ts_server_binary(
+ container_dir: PathBuf,
+ node: &NodeRuntime,
+) -> Option<LanguageServerBinary> {
+ (|| async move {
+ let old_server_path = container_dir.join(TypeScriptLspAdapter::OLD_SERVER_PATH);
+ let new_server_path = container_dir.join(TypeScriptLspAdapter::NEW_SERVER_PATH);
+ if new_server_path.exists() {
+ Ok(LanguageServerBinary {
+ path: node.binary_path().await?,
+ arguments: typescript_server_binary_arguments(&new_server_path),
+ })
+ } else if old_server_path.exists() {
+ Ok(LanguageServerBinary {
+ path: node.binary_path().await?,
+ arguments: typescript_server_binary_arguments(&old_server_path),
+ })
+ } else {
+ Err(anyhow!(
+ "missing executable in directory {:?}",
+ container_dir
+ ))
+ }
+ })()
+ .await
+ .log_err()
+}
+
pub struct EsLintLspAdapter {
node: Arc<NodeRuntime>,
}
@@ -249,11 +263,11 @@ impl LspAdapter for EsLintLspAdapter {
fs::rename(first.path(), &repo_root).await?;
self.node
- .run_npm_subcommand(&repo_root, "install", &[])
+ .run_npm_subcommand(Some(&repo_root), "install", &[])
.await?;
self.node
- .run_npm_subcommand(&repo_root, "run-script", &["compile"])
+ .run_npm_subcommand(Some(&repo_root), "run-script", &["compile"])
.await?;
}
@@ -268,21 +282,14 @@ impl LspAdapter for EsLintLspAdapter {
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
- (|| async move {
- // This is unfortunate but we don't know what the version is to build a path directly
- let mut dir = fs::read_dir(&container_dir).await?;
- let first = dir.next().await.ok_or(anyhow!("missing first file"))??;
- if !first.file_type().await?.is_dir() {
- return Err(anyhow!("First entry is not a directory"));
- }
+ get_cached_eslint_server_binary(container_dir, &self.node).await
+ }
- Ok(LanguageServerBinary {
- path: first.path().join(Self::SERVER_PATH),
- arguments: Default::default(),
- })
- })()
- .await
- .log_err()
+ async fn installation_test_binary(
+ &self,
+ container_dir: PathBuf,
+ ) -> Option<LanguageServerBinary> {
+ get_cached_eslint_server_binary(container_dir, &self.node).await
}
async fn label_for_completion(
@@ -298,6 +305,28 @@ impl LspAdapter for EsLintLspAdapter {
}
}
+async fn get_cached_eslint_server_binary(
+ container_dir: PathBuf,
+ node: &NodeRuntime,
+) -> Option<LanguageServerBinary> {
+ (|| async move {
+ // This is unfortunate but we don't know what the version is to build a path directly
+ let mut dir = fs::read_dir(&container_dir).await?;
+ let first = dir.next().await.ok_or(anyhow!("missing first file"))??;
+ if !first.file_type().await?.is_dir() {
+ return Err(anyhow!("First entry is not a directory"));
+ }
+ let server_path = first.path().join(EsLintLspAdapter::SERVER_PATH);
+
+ Ok(LanguageServerBinary {
+ path: node.binary_path().await?,
+ arguments: eslint_server_binary_arguments(&server_path),
+ })
+ })()
+ .await
+ .log_err()
+}
+
#[cfg(test)]
mod tests {
use gpui::TestAppContext;
@@ -3,9 +3,9 @@ use async_trait::async_trait;
use futures::{future::BoxFuture, FutureExt, StreamExt};
use gpui::AppContext;
use language::{
- language_settings::all_language_settings, LanguageServerBinary, LanguageServerName, LspAdapter,
- LspAdapterDelegate,
+ language_settings::all_language_settings, LanguageServerName, LspAdapter, LspAdapterDelegate,
};
+use lsp::LanguageServerBinary;
use node_runtime::NodeRuntime;
use serde_json::Value;
use smol::fs;
@@ -18,6 +18,8 @@ use std::{
};
use util::ResultExt;
+const SERVER_PATH: &'static str = "node_modules/yaml-language-server/bin/yaml-language-server";
+
fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
vec![server_path.into(), "--stdio".into()]
}
@@ -27,8 +29,6 @@ pub struct YamlLspAdapter {
}
impl YamlLspAdapter {
- const SERVER_PATH: &'static str = "node_modules/yaml-language-server/bin/yaml-language-server";
-
pub fn new(node: Arc<NodeRuntime>) -> Self {
YamlLspAdapter { node }
}
@@ -58,7 +58,7 @@ impl LspAdapter for YamlLspAdapter {
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let version = version.downcast::<String>().unwrap();
- let server_path = container_dir.join(Self::SERVER_PATH);
+ let server_path = container_dir.join(SERVER_PATH);
if fs::metadata(&server_path).await.is_err() {
self.node
@@ -77,33 +77,15 @@ impl LspAdapter for YamlLspAdapter {
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
- (|| async move {
- let mut last_version_dir = None;
- let mut entries = fs::read_dir(&container_dir).await?;
- while let Some(entry) = entries.next().await {
- let entry = entry?;
- if entry.file_type().await?.is_dir() {
- last_version_dir = Some(entry.path());
- }
- }
- let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
- let server_path = last_version_dir.join(Self::SERVER_PATH);
- if server_path.exists() {
- Ok(LanguageServerBinary {
- path: self.node.binary_path().await?,
- arguments: server_binary_arguments(&server_path),
- })
- } else {
- Err(anyhow!(
- "missing executable in directory {:?}",
- last_version_dir
- ))
- }
- })()
- .await
- .log_err()
+ get_cached_server_binary(container_dir, &self.node).await
}
+ async fn installation_test_binary(
+ &self,
+ container_dir: PathBuf,
+ ) -> Option<LanguageServerBinary> {
+ get_cached_server_binary(container_dir, &self.node).await
+ }
fn workspace_configuration(&self, cx: &mut AppContext) -> Option<BoxFuture<'static, Value>> {
let tab_size = all_language_settings(None, cx)
.language(Some("YAML"))
@@ -121,3 +103,34 @@ impl LspAdapter for YamlLspAdapter {
)
}
}
+
+async fn get_cached_server_binary(
+ container_dir: PathBuf,
+ node: &NodeRuntime,
+) -> Option<LanguageServerBinary> {
+ (|| async move {
+ let mut last_version_dir = None;
+ let mut entries = fs::read_dir(&container_dir).await?;
+ while let Some(entry) = entries.next().await {
+ let entry = entry?;
+ if entry.file_type().await?.is_dir() {
+ last_version_dir = Some(entry.path());
+ }
+ }
+ let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
+ let server_path = last_version_dir.join(SERVER_PATH);
+ if server_path.exists() {
+ Ok(LanguageServerBinary {
+ path: node.binary_path().await?,
+ arguments: server_binary_arguments(&server_path),
+ })
+ } else {
+ Err(anyhow!(
+ "missing executable in directory {:?}",
+ last_version_dir
+ ))
+ }
+ })()
+ .await
+ .log_err()
+}
@@ -131,7 +131,7 @@ fn main() {
languages.set_executor(cx.background().clone());
languages.set_language_server_download_dir(paths::LANGUAGES_DIR.clone());
let languages = Arc::new(languages);
- let node_runtime = NodeRuntime::new(http.clone(), cx.background().to_owned());
+ let node_runtime = NodeRuntime::instance(http.clone(), cx.background().to_owned());
languages::init(languages.clone(), node_runtime.clone());
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
@@ -2144,7 +2144,7 @@ mod tests {
languages.set_executor(cx.background().clone());
let languages = Arc::new(languages);
let http = FakeHttpClient::with_404_response();
- let node_runtime = NodeRuntime::new(http, cx.background().to_owned());
+ let node_runtime = NodeRuntime::instance(http, cx.background().to_owned());
languages::init(languages.clone(), node_runtime);
for name in languages.language_names() {
languages.language_for_name(&name);