mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-25 01:34:02 +00:00
Merge pull request #459 from zed-industries/spurious-macro-errors
Download language servers dynamically on startup
This commit is contained in:
commit
c752383042
23 changed files with 743 additions and 362 deletions
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
|
@ -34,11 +34,6 @@ jobs:
|
|||
with:
|
||||
clean: false
|
||||
|
||||
- name: Download rust-analyzer
|
||||
run: |
|
||||
script/download-rust-analyzer
|
||||
echo "$PWD/vendor/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Run tests
|
||||
run: cargo test --workspace --no-fail-fast
|
||||
|
||||
|
@ -69,14 +64,11 @@ jobs:
|
|||
uses: actions/checkout@v2
|
||||
with:
|
||||
clean: false
|
||||
|
||||
|
||||
- name: Validate version
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
run: script/validate-version
|
||||
|
||||
- name: Download rust-analyzer
|
||||
run: script/download-rust-analyzer
|
||||
|
||||
- name: Create app bundle
|
||||
run: script/bundle
|
||||
|
||||
|
|
19
Cargo.lock
generated
19
Cargo.lock
generated
|
@ -178,6 +178,17 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-broadcast"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90622698a1218e0b2fb846c97b5f19a0831f6baddee73d9454156365ccfa473b"
|
||||
dependencies = [
|
||||
"easy-parallel",
|
||||
"event-listener",
|
||||
"futures-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-channel"
|
||||
version = "1.6.1"
|
||||
|
@ -1926,9 +1937,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.12"
|
||||
version = "0.3.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79e5145dde8da7d1b3892dad07a9c98fc04bc39892b1ecc9692cf53e2b780a65"
|
||||
checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
|
@ -2619,7 +2630,9 @@ name = "language"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-broadcast",
|
||||
"async-trait",
|
||||
"client",
|
||||
"clock",
|
||||
"collections",
|
||||
"ctor",
|
||||
|
@ -5701,6 +5714,7 @@ dependencies = [
|
|||
"client",
|
||||
"clock",
|
||||
"collections",
|
||||
"futures",
|
||||
"gpui",
|
||||
"language",
|
||||
"log",
|
||||
|
@ -5741,6 +5755,7 @@ name = "zed"
|
|||
version = "0.15.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-compression",
|
||||
"async-recursion",
|
||||
"async-trait",
|
||||
"chat_panel",
|
||||
|
|
|
@ -224,6 +224,10 @@ impl Client {
|
|||
self.id
|
||||
}
|
||||
|
||||
pub fn http_client(&self) -> Arc<dyn HttpClient> {
|
||||
self.http.clone()
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn override_authenticate<F>(&mut self, authenticate: F) -> &mut Self
|
||||
where
|
||||
|
|
|
@ -410,8 +410,6 @@ impl View for DiagnosticMessage {
|
|||
diagnostic.message.split('\n').next().unwrap().to_string(),
|
||||
theme.diagnostic_message.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_margin_left(theme.item_spacing)
|
||||
.boxed()
|
||||
} else {
|
||||
Empty::new().boxed()
|
||||
|
|
|
@ -9,6 +9,7 @@ path = "src/language.rs"
|
|||
[features]
|
||||
test-support = [
|
||||
"rand",
|
||||
"client/test-support",
|
||||
"collections/test-support",
|
||||
"lsp/test-support",
|
||||
"text/test-support",
|
||||
|
@ -17,6 +18,7 @@ test-support = [
|
|||
]
|
||||
|
||||
[dependencies]
|
||||
client = { path = "../client" }
|
||||
clock = { path = "../clock" }
|
||||
collections = { path = "../collections" }
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
|
@ -28,6 +30,7 @@ text = { path = "../text" }
|
|||
theme = { path = "../theme" }
|
||||
util = { path = "../util" }
|
||||
anyhow = "1.0.38"
|
||||
async-broadcast = "0.3.4"
|
||||
async-trait = "0.1"
|
||||
futures = "0.3"
|
||||
lazy_static = "1.4"
|
||||
|
@ -44,6 +47,7 @@ tree-sitter = "0.20"
|
|||
tree-sitter-rust = { version = "0.20.0", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
client = { path = "../client", features = ["test-support"] }
|
||||
collections = { path = "../collections", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
lsp = { path = "../lsp", features = ["test-support"] }
|
||||
|
|
|
@ -7,15 +7,27 @@ pub mod proto;
|
|||
mod tests;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use client::http::{self, HttpClient};
|
||||
use collections::HashSet;
|
||||
use gpui::AppContext;
|
||||
use futures::{
|
||||
future::{BoxFuture, Shared},
|
||||
FutureExt, TryFutureExt,
|
||||
};
|
||||
use gpui::{AppContext, Task};
|
||||
use highlight_map::HighlightMap;
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use serde::Deserialize;
|
||||
use std::{cell::RefCell, ops::Range, path::Path, str, sync::Arc};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
str,
|
||||
sync::Arc,
|
||||
};
|
||||
use theme::SyntaxTheme;
|
||||
use tree_sitter::{self, Query};
|
||||
use util::ResultExt;
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use futures::channel::mpsc;
|
||||
|
@ -47,7 +59,23 @@ pub trait ToLspPosition {
|
|||
fn to_lsp_position(self) -> lsp::Position;
|
||||
}
|
||||
|
||||
pub trait LspPostProcessor: 'static + Send + Sync {
|
||||
pub struct LspBinaryVersion {
|
||||
pub name: String,
|
||||
pub url: http::Url,
|
||||
}
|
||||
|
||||
pub trait LspExt: 'static + Send + Sync {
|
||||
fn fetch_latest_server_version(
|
||||
&self,
|
||||
http: Arc<dyn HttpClient>,
|
||||
) -> BoxFuture<'static, Result<LspBinaryVersion>>;
|
||||
fn fetch_server_binary(
|
||||
&self,
|
||||
version: LspBinaryVersion,
|
||||
http: Arc<dyn HttpClient>,
|
||||
download_dir: Arc<Path>,
|
||||
) -> BoxFuture<'static, Result<PathBuf>>;
|
||||
fn cached_server_binary(&self, download_dir: Arc<Path>) -> BoxFuture<'static, Option<PathBuf>>;
|
||||
fn process_diagnostics(&self, diagnostics: &mut lsp::PublishDiagnosticsParams);
|
||||
fn label_for_completion(
|
||||
&self,
|
||||
|
@ -77,7 +105,6 @@ pub struct LanguageConfig {
|
|||
|
||||
#[derive(Default, Deserialize)]
|
||||
pub struct LanguageServerConfig {
|
||||
pub binary: String,
|
||||
pub disk_based_diagnostic_sources: HashSet<String>,
|
||||
pub disk_based_diagnostics_progress_token: Option<String>,
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
|
@ -103,7 +130,8 @@ pub struct BracketPair {
|
|||
pub struct Language {
|
||||
pub(crate) config: LanguageConfig,
|
||||
pub(crate) grammar: Option<Arc<Grammar>>,
|
||||
pub(crate) lsp_post_processor: Option<Box<dyn LspPostProcessor>>,
|
||||
pub(crate) lsp_ext: Option<Arc<dyn LspExt>>,
|
||||
lsp_binary_path: Mutex<Option<Shared<BoxFuture<'static, Result<PathBuf, Arc<anyhow::Error>>>>>>,
|
||||
}
|
||||
|
||||
pub struct Grammar {
|
||||
|
@ -115,18 +143,35 @@ pub struct Grammar {
|
|||
pub(crate) highlight_map: Mutex<HighlightMap>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Clone)]
|
||||
pub enum LanguageServerBinaryStatus {
|
||||
CheckingForUpdate,
|
||||
Downloading,
|
||||
Downloaded,
|
||||
Cached,
|
||||
Failed,
|
||||
}
|
||||
|
||||
pub struct LanguageRegistry {
|
||||
languages: Vec<Arc<Language>>,
|
||||
language_server_download_dir: Option<Arc<Path>>,
|
||||
lsp_binary_statuses_tx: async_broadcast::Sender<(Arc<Language>, LanguageServerBinaryStatus)>,
|
||||
lsp_binary_statuses_rx: async_broadcast::Receiver<(Arc<Language>, LanguageServerBinaryStatus)>,
|
||||
}
|
||||
|
||||
impl LanguageRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
let (lsp_binary_statuses_tx, lsp_binary_statuses_rx) = async_broadcast::broadcast(16);
|
||||
Self {
|
||||
language_server_download_dir: None,
|
||||
languages: Default::default(),
|
||||
lsp_binary_statuses_tx,
|
||||
lsp_binary_statuses_rx,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add(&mut self, language: Arc<Language>) {
|
||||
self.languages.push(language);
|
||||
self.languages.push(language.clone());
|
||||
}
|
||||
|
||||
pub fn set_theme(&self, theme: &SyntaxTheme) {
|
||||
|
@ -135,6 +180,10 @@ impl LanguageRegistry {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn set_language_server_download_dir(&mut self, path: impl Into<Arc<Path>>) {
|
||||
self.language_server_download_dir = Some(path.into());
|
||||
}
|
||||
|
||||
pub fn get_language(&self, name: &str) -> Option<&Arc<Language>> {
|
||||
self.languages
|
||||
.iter()
|
||||
|
@ -154,6 +203,140 @@ impl LanguageRegistry {
|
|||
.any(|suffix| path_suffixes.contains(&Some(suffix.as_str())))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn start_language_server(
|
||||
&self,
|
||||
language: &Arc<Language>,
|
||||
root_path: Arc<Path>,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
cx: &AppContext,
|
||||
) -> Option<Task<Result<Arc<lsp::LanguageServer>>>> {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
if let Some(config) = &language.config.language_server {
|
||||
if let Some(fake_config) = &config.fake_config {
|
||||
use postage::prelude::Stream;
|
||||
|
||||
let (server, mut fake_server) = lsp::LanguageServer::fake_with_capabilities(
|
||||
fake_config.capabilities.clone(),
|
||||
cx.background().clone(),
|
||||
);
|
||||
|
||||
if let Some(initalizer) = &fake_config.initializer {
|
||||
initalizer(&mut fake_server);
|
||||
}
|
||||
|
||||
let servers_tx = fake_config.servers_tx.clone();
|
||||
let mut initialized = server.capabilities();
|
||||
cx.background()
|
||||
.spawn(async move {
|
||||
while initialized.recv().await.is_none() {}
|
||||
servers_tx.unbounded_send(fake_server).ok();
|
||||
})
|
||||
.detach();
|
||||
|
||||
return Some(Task::ready(Ok(server.clone())));
|
||||
}
|
||||
}
|
||||
|
||||
let download_dir = self
|
||||
.language_server_download_dir
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow!("language server download directory has not been assigned"))
|
||||
.log_err()?;
|
||||
|
||||
let lsp_ext = language.lsp_ext.clone()?;
|
||||
let background = cx.background().clone();
|
||||
let server_binary_path = {
|
||||
Some(
|
||||
language
|
||||
.lsp_binary_path
|
||||
.lock()
|
||||
.get_or_insert_with(|| {
|
||||
get_server_binary_path(
|
||||
lsp_ext,
|
||||
language.clone(),
|
||||
http_client,
|
||||
download_dir,
|
||||
self.lsp_binary_statuses_tx.clone(),
|
||||
)
|
||||
.map_err(Arc::new)
|
||||
.boxed()
|
||||
.shared()
|
||||
})
|
||||
.clone()
|
||||
.map_err(|e| anyhow!(e)),
|
||||
)
|
||||
}?;
|
||||
Some(cx.background().spawn(async move {
|
||||
let server_binary_path = server_binary_path.await?;
|
||||
let server = lsp::LanguageServer::new(&server_binary_path, &root_path, background)?;
|
||||
Ok(server)
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn language_server_binary_statuses(
|
||||
&self,
|
||||
) -> async_broadcast::Receiver<(Arc<Language>, LanguageServerBinaryStatus)> {
|
||||
self.lsp_binary_statuses_rx.clone()
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_server_binary_path(
|
||||
lsp_ext: Arc<dyn LspExt>,
|
||||
language: Arc<Language>,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
download_dir: Arc<Path>,
|
||||
statuses: async_broadcast::Sender<(Arc<Language>, LanguageServerBinaryStatus)>,
|
||||
) -> Result<PathBuf> {
|
||||
let path = fetch_latest_server_binary_path(
|
||||
lsp_ext.clone(),
|
||||
language.clone(),
|
||||
http_client,
|
||||
download_dir.clone(),
|
||||
statuses.clone(),
|
||||
)
|
||||
.await;
|
||||
if path.is_err() {
|
||||
if let Some(cached_path) = lsp_ext.cached_server_binary(download_dir).await {
|
||||
statuses
|
||||
.broadcast((language.clone(), LanguageServerBinaryStatus::Cached))
|
||||
.await?;
|
||||
return Ok(cached_path);
|
||||
} else {
|
||||
statuses
|
||||
.broadcast((language.clone(), LanguageServerBinaryStatus::Failed))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
path
|
||||
}
|
||||
|
||||
async fn fetch_latest_server_binary_path(
|
||||
lsp_ext: Arc<dyn LspExt>,
|
||||
language: Arc<Language>,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
download_dir: Arc<Path>,
|
||||
lsp_binary_statuses_tx: async_broadcast::Sender<(Arc<Language>, LanguageServerBinaryStatus)>,
|
||||
) -> Result<PathBuf> {
|
||||
lsp_binary_statuses_tx
|
||||
.broadcast((
|
||||
language.clone(),
|
||||
LanguageServerBinaryStatus::CheckingForUpdate,
|
||||
))
|
||||
.await?;
|
||||
let version_info = lsp_ext
|
||||
.fetch_latest_server_version(http_client.clone())
|
||||
.await?;
|
||||
lsp_binary_statuses_tx
|
||||
.broadcast((language.clone(), LanguageServerBinaryStatus::Downloading))
|
||||
.await?;
|
||||
let path = lsp_ext
|
||||
.fetch_server_binary(version_info, http_client, download_dir)
|
||||
.await?;
|
||||
lsp_binary_statuses_tx
|
||||
.broadcast((language.clone(), LanguageServerBinaryStatus::Downloaded))
|
||||
.await?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
impl Language {
|
||||
|
@ -170,7 +353,8 @@ impl Language {
|
|||
highlight_map: Default::default(),
|
||||
})
|
||||
}),
|
||||
lsp_post_processor: None,
|
||||
lsp_ext: None,
|
||||
lsp_binary_path: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -214,8 +398,8 @@ impl Language {
|
|||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn with_lsp_post_processor(mut self, processor: impl LspPostProcessor) -> Self {
|
||||
self.lsp_post_processor = Some(Box::new(processor));
|
||||
pub fn with_lsp_ext(mut self, lsp_ext: impl LspExt) -> Self {
|
||||
self.lsp_ext = Some(Arc::new(lsp_ext));
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -227,50 +411,6 @@ impl Language {
|
|||
self.config.line_comment.as_deref()
|
||||
}
|
||||
|
||||
pub fn start_server(
|
||||
&self,
|
||||
root_path: &Path,
|
||||
cx: &AppContext,
|
||||
) -> Result<Option<Arc<lsp::LanguageServer>>> {
|
||||
if let Some(config) = &self.config.language_server {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
if let Some(fake_config) = &config.fake_config {
|
||||
use postage::prelude::Stream;
|
||||
|
||||
let (server, mut fake_server) = lsp::LanguageServer::fake_with_capabilities(
|
||||
fake_config.capabilities.clone(),
|
||||
cx.background().clone(),
|
||||
);
|
||||
|
||||
if let Some(initalizer) = &fake_config.initializer {
|
||||
initalizer(&mut fake_server);
|
||||
}
|
||||
|
||||
let servers_tx = fake_config.servers_tx.clone();
|
||||
let mut initialized = server.capabilities();
|
||||
cx.background()
|
||||
.spawn(async move {
|
||||
while initialized.recv().await.is_none() {}
|
||||
servers_tx.unbounded_send(fake_server).ok();
|
||||
})
|
||||
.detach();
|
||||
|
||||
return Ok(Some(server.clone()));
|
||||
}
|
||||
|
||||
const ZED_BUNDLE: Option<&'static str> = option_env!("ZED_BUNDLE");
|
||||
let binary_path = if ZED_BUNDLE.map_or(Ok(false), |b| b.parse())? {
|
||||
cx.platform()
|
||||
.path_for_resource(Some(&config.binary), None)?
|
||||
} else {
|
||||
Path::new(&config.binary).to_path_buf()
|
||||
};
|
||||
lsp::LanguageServer::new(&binary_path, root_path, cx.background().clone()).map(Some)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn disk_based_diagnostic_sources(&self) -> Option<&HashSet<String>> {
|
||||
self.config
|
||||
.language_server
|
||||
|
@ -286,7 +426,7 @@ impl Language {
|
|||
}
|
||||
|
||||
pub fn process_diagnostics(&self, diagnostics: &mut lsp::PublishDiagnosticsParams) {
|
||||
if let Some(processor) = self.lsp_post_processor.as_ref() {
|
||||
if let Some(processor) = self.lsp_ext.as_ref() {
|
||||
processor.process_diagnostics(diagnostics);
|
||||
}
|
||||
}
|
||||
|
@ -295,7 +435,7 @@ impl Language {
|
|||
&self,
|
||||
completion: &lsp::CompletionItem,
|
||||
) -> Option<CompletionLabel> {
|
||||
self.lsp_post_processor
|
||||
self.lsp_ext
|
||||
.as_ref()?
|
||||
.label_for_completion(completion, self)
|
||||
}
|
||||
|
|
|
@ -22,28 +22,25 @@ fn init_logger() {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[gpui::test]
|
||||
fn test_select_language() {
|
||||
let registry = LanguageRegistry {
|
||||
languages: vec![
|
||||
Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".to_string(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
)),
|
||||
Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
name: "Make".to_string(),
|
||||
path_suffixes: vec!["Makefile".to_string(), "mk".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
)),
|
||||
],
|
||||
};
|
||||
let mut registry = LanguageRegistry::new();
|
||||
registry.add(Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".to_string(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
)));
|
||||
registry.add(Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
name: "Make".to_string(),
|
||||
path_suffixes: vec!["Makefile".to_string(), "mk".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
)));
|
||||
|
||||
// matching file extension
|
||||
assert_eq!(
|
||||
|
|
|
@ -700,8 +700,6 @@ impl FakeLanguageServer {
|
|||
mod tests {
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
use unindent::Unindent;
|
||||
use util::test::temp_tree;
|
||||
|
||||
#[ctor::ctor]
|
||||
fn init_logger() {
|
||||
|
@ -710,64 +708,6 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_rust_analyzer(cx: TestAppContext) {
|
||||
let lib_source = r#"
|
||||
fn fun() {
|
||||
let hello = "world";
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
let root_dir = temp_tree(json!({
|
||||
"Cargo.toml": r#"
|
||||
[package]
|
||||
name = "temp"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
"#.unindent(),
|
||||
"src": {
|
||||
"lib.rs": &lib_source
|
||||
}
|
||||
}));
|
||||
let lib_file_uri = Url::from_file_path(root_dir.path().join("src/lib.rs")).unwrap();
|
||||
|
||||
let server =
|
||||
LanguageServer::new(Path::new("rust-analyzer"), root_dir.path(), cx.background())
|
||||
.unwrap();
|
||||
server.next_idle_notification().await;
|
||||
|
||||
server
|
||||
.notify::<notification::DidOpenTextDocument>(DidOpenTextDocumentParams {
|
||||
text_document: TextDocumentItem::new(
|
||||
lib_file_uri.clone(),
|
||||
"rust".to_string(),
|
||||
0,
|
||||
lib_source,
|
||||
),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let hover = server
|
||||
.request::<request::HoverRequest>(HoverParams {
|
||||
text_document_position_params: TextDocumentPositionParams {
|
||||
text_document: TextDocumentIdentifier::new(lib_file_uri),
|
||||
position: Position::new(1, 21),
|
||||
},
|
||||
work_done_progress_params: Default::default(),
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
hover.contents,
|
||||
HoverContents::Markup(MarkupContent {
|
||||
kind: MarkupKind::PlainText,
|
||||
value: "&str".to_string()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_fake(cx: TestAppContext) {
|
||||
let (server, mut fake) = LanguageServer::fake(cx.background());
|
||||
|
@ -828,19 +768,6 @@ mod tests {
|
|||
fake.receive_notification::<notification::Exit>().await;
|
||||
}
|
||||
|
||||
impl LanguageServer {
|
||||
async fn next_idle_notification(self: &Arc<Self>) {
|
||||
let (tx, rx) = channel::unbounded();
|
||||
let _subscription =
|
||||
self.on_notification::<ServerStatusNotification, _>(move |params| {
|
||||
if params.quiescent {
|
||||
tx.try_send(()).unwrap();
|
||||
}
|
||||
});
|
||||
let _ = rx.recv().await;
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ServerStatusNotification {}
|
||||
|
||||
impl notification::Notification for ServerStatusNotification {
|
||||
|
|
|
@ -7,7 +7,7 @@ use anyhow::{anyhow, Context, Result};
|
|||
use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore};
|
||||
use clock::ReplicaId;
|
||||
use collections::{hash_map, HashMap, HashSet};
|
||||
use futures::Future;
|
||||
use futures::{future::Shared, Future, FutureExt};
|
||||
use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet};
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task,
|
||||
|
@ -39,6 +39,8 @@ pub struct Project {
|
|||
active_entry: Option<ProjectEntry>,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
language_servers: HashMap<(WorktreeId, String), Arc<LanguageServer>>,
|
||||
started_language_servers:
|
||||
HashMap<(WorktreeId, String), Shared<Task<Option<Arc<LanguageServer>>>>>,
|
||||
client: Arc<client::Client>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
fs: Arc<dyn Fs>,
|
||||
|
@ -258,6 +260,7 @@ impl Project {
|
|||
fs,
|
||||
language_servers_with_diagnostics_running: 0,
|
||||
language_servers: Default::default(),
|
||||
started_language_servers: Default::default(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -309,6 +312,7 @@ impl Project {
|
|||
},
|
||||
language_servers_with_diagnostics_running: 0,
|
||||
language_servers: Default::default(),
|
||||
started_language_servers: Default::default(),
|
||||
};
|
||||
for worktree in worktrees {
|
||||
this.add_worktree(&worktree, cx);
|
||||
|
@ -776,7 +780,7 @@ impl Project {
|
|||
};
|
||||
|
||||
// If the buffer has a language, set it and start/assign the language server
|
||||
if let Some(language) = self.languages.select_language(&full_path) {
|
||||
if let Some(language) = self.languages.select_language(&full_path).cloned() {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.set_language(Some(language.clone()), cx);
|
||||
});
|
||||
|
@ -786,24 +790,20 @@ impl Project {
|
|||
if let Some(local_worktree) = worktree.and_then(|w| w.read(cx).as_local()) {
|
||||
let worktree_id = local_worktree.id();
|
||||
let worktree_abs_path = local_worktree.abs_path().clone();
|
||||
let buffer = buffer.downgrade();
|
||||
let language_server =
|
||||
self.start_language_server(worktree_id, worktree_abs_path, language, cx);
|
||||
|
||||
let language_server = match self
|
||||
.language_servers
|
||||
.entry((worktree_id, language.name().to_string()))
|
||||
{
|
||||
hash_map::Entry::Occupied(e) => Some(e.get().clone()),
|
||||
hash_map::Entry::Vacant(e) => Self::start_language_server(
|
||||
self.client.clone(),
|
||||
language.clone(),
|
||||
&worktree_abs_path,
|
||||
cx,
|
||||
)
|
||||
.map(|server| e.insert(server).clone()),
|
||||
};
|
||||
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.set_language_server(language_server, cx);
|
||||
});
|
||||
cx.spawn_weak(|_, mut cx| async move {
|
||||
if let Some(language_server) = language_server.await {
|
||||
if let Some(buffer) = buffer.upgrade(&cx) {
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.set_language_server(Some(language_server), cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -819,116 +819,151 @@ impl Project {
|
|||
}
|
||||
|
||||
fn start_language_server(
|
||||
rpc: Arc<Client>,
|
||||
&mut self,
|
||||
worktree_id: WorktreeId,
|
||||
worktree_path: Arc<Path>,
|
||||
language: Arc<Language>,
|
||||
worktree_path: &Path,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Option<Arc<LanguageServer>> {
|
||||
) -> Shared<Task<Option<Arc<LanguageServer>>>> {
|
||||
enum LspEvent {
|
||||
DiagnosticsStart,
|
||||
DiagnosticsUpdate(lsp::PublishDiagnosticsParams),
|
||||
DiagnosticsFinish,
|
||||
}
|
||||
|
||||
let language_server = language
|
||||
.start_server(worktree_path, cx)
|
||||
.log_err()
|
||||
.flatten()?;
|
||||
let disk_based_sources = language
|
||||
.disk_based_diagnostic_sources()
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let disk_based_diagnostics_progress_token =
|
||||
language.disk_based_diagnostics_progress_token().cloned();
|
||||
let has_disk_based_diagnostic_progress_token =
|
||||
disk_based_diagnostics_progress_token.is_some();
|
||||
let (diagnostics_tx, diagnostics_rx) = smol::channel::unbounded();
|
||||
|
||||
// Listen for `PublishDiagnostics` notifications.
|
||||
language_server
|
||||
.on_notification::<lsp::notification::PublishDiagnostics, _>({
|
||||
let diagnostics_tx = diagnostics_tx.clone();
|
||||
move |params| {
|
||||
if !has_disk_based_diagnostic_progress_token {
|
||||
block_on(diagnostics_tx.send(LspEvent::DiagnosticsStart)).ok();
|
||||
let key = (worktree_id, language.name().to_string());
|
||||
self.started_language_servers
|
||||
.entry(key.clone())
|
||||
.or_insert_with(|| {
|
||||
let language_server = self.languages.start_language_server(
|
||||
&language,
|
||||
worktree_path,
|
||||
self.client.http_client(),
|
||||
cx,
|
||||
);
|
||||
let rpc = self.client.clone();
|
||||
cx.spawn_weak(|this, mut cx| async move {
|
||||
let language_server = language_server?.await.log_err()?;
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.language_servers.insert(key, language_server.clone());
|
||||
});
|
||||
}
|
||||
block_on(diagnostics_tx.send(LspEvent::DiagnosticsUpdate(params))).ok();
|
||||
if !has_disk_based_diagnostic_progress_token {
|
||||
block_on(diagnostics_tx.send(LspEvent::DiagnosticsFinish)).ok();
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
// Listen for `Progress` notifications. Send an event when the language server
|
||||
// transitions between running jobs and not running any jobs.
|
||||
let mut running_jobs_for_this_server: i32 = 0;
|
||||
language_server
|
||||
.on_notification::<lsp::notification::Progress, _>(move |params| {
|
||||
let token = match params.token {
|
||||
lsp::NumberOrString::Number(_) => None,
|
||||
lsp::NumberOrString::String(token) => Some(token),
|
||||
};
|
||||
let disk_based_sources = language
|
||||
.disk_based_diagnostic_sources()
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let disk_based_diagnostics_progress_token =
|
||||
language.disk_based_diagnostics_progress_token().cloned();
|
||||
let has_disk_based_diagnostic_progress_token =
|
||||
disk_based_diagnostics_progress_token.is_some();
|
||||
let (diagnostics_tx, diagnostics_rx) = smol::channel::unbounded();
|
||||
|
||||
if token == disk_based_diagnostics_progress_token {
|
||||
match params.value {
|
||||
lsp::ProgressParamsValue::WorkDone(progress) => match progress {
|
||||
lsp::WorkDoneProgress::Begin(_) => {
|
||||
running_jobs_for_this_server += 1;
|
||||
if running_jobs_for_this_server == 1 {
|
||||
// Listen for `PublishDiagnostics` notifications.
|
||||
language_server
|
||||
.on_notification::<lsp::notification::PublishDiagnostics, _>({
|
||||
let diagnostics_tx = diagnostics_tx.clone();
|
||||
move |params| {
|
||||
if !has_disk_based_diagnostic_progress_token {
|
||||
block_on(diagnostics_tx.send(LspEvent::DiagnosticsStart)).ok();
|
||||
}
|
||||
}
|
||||
lsp::WorkDoneProgress::End(_) => {
|
||||
running_jobs_for_this_server -= 1;
|
||||
if running_jobs_for_this_server == 0 {
|
||||
block_on(diagnostics_tx.send(LspEvent::DiagnosticsUpdate(params)))
|
||||
.ok();
|
||||
if !has_disk_based_diagnostic_progress_token {
|
||||
block_on(diagnostics_tx.send(LspEvent::DiagnosticsFinish)).ok();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
// Listen for `Progress` notifications. Send an event when the language server
|
||||
// transitions between running jobs and not running any jobs.
|
||||
let mut running_jobs_for_this_server: i32 = 0;
|
||||
language_server
|
||||
.on_notification::<lsp::notification::Progress, _>(move |params| {
|
||||
let token = match params.token {
|
||||
lsp::NumberOrString::Number(_) => None,
|
||||
lsp::NumberOrString::String(token) => Some(token),
|
||||
};
|
||||
|
||||
if token == disk_based_diagnostics_progress_token {
|
||||
match params.value {
|
||||
lsp::ProgressParamsValue::WorkDone(progress) => {
|
||||
match progress {
|
||||
lsp::WorkDoneProgress::Begin(_) => {
|
||||
running_jobs_for_this_server += 1;
|
||||
if running_jobs_for_this_server == 1 {
|
||||
block_on(
|
||||
diagnostics_tx
|
||||
.send(LspEvent::DiagnosticsStart),
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
lsp::WorkDoneProgress::End(_) => {
|
||||
running_jobs_for_this_server -= 1;
|
||||
if running_jobs_for_this_server == 0 {
|
||||
block_on(
|
||||
diagnostics_tx
|
||||
.send(LspEvent::DiagnosticsFinish),
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
// Process all the LSP events.
|
||||
cx.spawn(|mut cx| async move {
|
||||
while let Ok(message) = diagnostics_rx.recv().await {
|
||||
let this = this.upgrade(&cx)?;
|
||||
match message {
|
||||
LspEvent::DiagnosticsStart => {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.disk_based_diagnostics_started(cx);
|
||||
if let Some(project_id) = this.remote_id() {
|
||||
rpc.send(proto::DiskBasedDiagnosticsUpdating {
|
||||
project_id,
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
});
|
||||
}
|
||||
LspEvent::DiagnosticsUpdate(mut params) => {
|
||||
language.process_diagnostics(&mut params);
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update_diagnostics(params, &disk_based_sources, cx)
|
||||
.log_err();
|
||||
});
|
||||
}
|
||||
LspEvent::DiagnosticsFinish => {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.disk_based_diagnostics_finished(cx);
|
||||
if let Some(project_id) = this.remote_id() {
|
||||
rpc.send(proto::DiskBasedDiagnosticsUpdated {
|
||||
project_id,
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(())
|
||||
})
|
||||
.detach();
|
||||
|
||||
Some(language_server)
|
||||
})
|
||||
.shared()
|
||||
})
|
||||
.detach();
|
||||
|
||||
// Process all the LSP events.
|
||||
cx.spawn_weak(|this, mut cx| async move {
|
||||
while let Ok(message) = diagnostics_rx.recv().await {
|
||||
let this = this.upgrade(&cx)?;
|
||||
match message {
|
||||
LspEvent::DiagnosticsStart => {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.disk_based_diagnostics_started(cx);
|
||||
if let Some(project_id) = this.remote_id() {
|
||||
rpc.send(proto::DiskBasedDiagnosticsUpdating { project_id })
|
||||
.log_err();
|
||||
}
|
||||
});
|
||||
}
|
||||
LspEvent::DiagnosticsUpdate(mut params) => {
|
||||
language.process_diagnostics(&mut params);
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update_diagnostics(params, &disk_based_sources, cx)
|
||||
.log_err();
|
||||
});
|
||||
}
|
||||
LspEvent::DiagnosticsFinish => {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.disk_based_diagnostics_finished(cx);
|
||||
if let Some(project_id) = this.remote_id() {
|
||||
rpc.send(proto::DiskBasedDiagnosticsUpdated { project_id })
|
||||
.log_err();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(())
|
||||
})
|
||||
.detach();
|
||||
|
||||
Some(language_server)
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub fn update_diagnostics(
|
||||
|
@ -2857,8 +2892,6 @@ impl Entity for Project {
|
|||
&mut self,
|
||||
_: &mut MutableAppContext,
|
||||
) -> Option<std::pin::Pin<Box<dyn 'static + Future<Output = ()>>>> {
|
||||
use futures::FutureExt;
|
||||
|
||||
let shutdown_futures = self
|
||||
.language_servers
|
||||
.drain()
|
||||
|
|
|
@ -2001,9 +2001,8 @@ mod tests {
|
|||
|
||||
// Set up a fake language server.
|
||||
let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake();
|
||||
Arc::get_mut(&mut lang_registry)
|
||||
.unwrap()
|
||||
.add(Arc::new(Language::new(
|
||||
Arc::get_mut(&mut lang_registry).unwrap().add(
|
||||
Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".to_string(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
|
@ -2011,7 +2010,9 @@ mod tests {
|
|||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
)));
|
||||
)),
|
||||
|
||||
);
|
||||
|
||||
// Connect to a server as 2 clients.
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
|
@ -2232,9 +2233,8 @@ mod tests {
|
|||
}),
|
||||
..Default::default()
|
||||
});
|
||||
Arc::get_mut(&mut lang_registry)
|
||||
.unwrap()
|
||||
.add(Arc::new(Language::new(
|
||||
Arc::get_mut(&mut lang_registry).unwrap().add(
|
||||
Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".to_string(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
|
@ -2242,7 +2242,9 @@ mod tests {
|
|||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
)));
|
||||
)),
|
||||
|
||||
);
|
||||
|
||||
// Connect to a server as 2 clients.
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
|
@ -2434,9 +2436,8 @@ mod tests {
|
|||
|
||||
// Set up a fake language server.
|
||||
let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake();
|
||||
Arc::get_mut(&mut lang_registry)
|
||||
.unwrap()
|
||||
.add(Arc::new(Language::new(
|
||||
Arc::get_mut(&mut lang_registry).unwrap().add(
|
||||
Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".to_string(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
|
@ -2444,7 +2445,9 @@ mod tests {
|
|||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
)));
|
||||
)),
|
||||
|
||||
);
|
||||
|
||||
// Connect to a server as 2 clients.
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
|
@ -2551,9 +2554,8 @@ mod tests {
|
|||
|
||||
// Set up a fake language server.
|
||||
let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake();
|
||||
Arc::get_mut(&mut lang_registry)
|
||||
.unwrap()
|
||||
.add(Arc::new(Language::new(
|
||||
Arc::get_mut(&mut lang_registry).unwrap().add(
|
||||
Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".to_string(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
|
@ -2561,7 +2563,9 @@ mod tests {
|
|||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
)));
|
||||
)),
|
||||
|
||||
);
|
||||
|
||||
// Connect to a server as 2 clients.
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
|
@ -2699,9 +2703,8 @@ mod tests {
|
|||
// Set up a fake language server.
|
||||
let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake();
|
||||
|
||||
Arc::get_mut(&mut lang_registry)
|
||||
.unwrap()
|
||||
.add(Arc::new(Language::new(
|
||||
Arc::get_mut(&mut lang_registry).unwrap().add(
|
||||
Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".to_string(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
|
@ -2709,7 +2712,9 @@ mod tests {
|
|||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
)));
|
||||
)),
|
||||
|
||||
);
|
||||
|
||||
// Connect to a server as 2 clients.
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
|
@ -2800,9 +2805,8 @@ mod tests {
|
|||
|
||||
// Set up a fake language server.
|
||||
let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake();
|
||||
Arc::get_mut(&mut lang_registry)
|
||||
.unwrap()
|
||||
.add(Arc::new(Language::new(
|
||||
Arc::get_mut(&mut lang_registry).unwrap().add(
|
||||
Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".to_string(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
|
@ -2810,7 +2814,9 @@ mod tests {
|
|||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
)));
|
||||
)),
|
||||
|
||||
);
|
||||
|
||||
// Connect to a server as 2 clients.
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
|
@ -3039,9 +3045,8 @@ mod tests {
|
|||
|
||||
// Set up a fake language server.
|
||||
let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake();
|
||||
Arc::get_mut(&mut lang_registry)
|
||||
.unwrap()
|
||||
.add(Arc::new(Language::new(
|
||||
Arc::get_mut(&mut lang_registry).unwrap().add(
|
||||
Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".to_string(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
|
@ -3049,7 +3054,9 @@ mod tests {
|
|||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
)));
|
||||
)),
|
||||
|
||||
);
|
||||
|
||||
// Connect to a server as 2 clients.
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
|
@ -3845,9 +3852,8 @@ mod tests {
|
|||
});
|
||||
});
|
||||
|
||||
Arc::get_mut(&mut host_lang_registry)
|
||||
.unwrap()
|
||||
.add(Arc::new(Language::new(
|
||||
Arc::get_mut(&mut host_lang_registry).unwrap().add(
|
||||
Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".to_string(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
|
@ -3855,7 +3861,9 @@ mod tests {
|
|||
..Default::default()
|
||||
},
|
||||
None,
|
||||
)));
|
||||
)),
|
||||
|
||||
);
|
||||
|
||||
let fs = FakeFs::new(cx.background());
|
||||
fs.insert_tree(
|
||||
|
|
|
@ -141,6 +141,7 @@ pub struct StatusBar {
|
|||
pub item_spacing: f32,
|
||||
pub cursor_position: TextStyle,
|
||||
pub diagnostic_message: TextStyle,
|
||||
pub lsp_message: TextStyle,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
|
|
|
@ -19,6 +19,7 @@ project = { path = "../project" }
|
|||
theme = { path = "../theme" }
|
||||
util = { path = "../util" }
|
||||
anyhow = "1.0.38"
|
||||
futures = "0.3"
|
||||
log = "0.4"
|
||||
parking_lot = "0.11.1"
|
||||
postage = { version = "0.4.1", features = ["futures-traits"] }
|
||||
|
|
137
crates/workspace/src/lsp_status.rs
Normal file
137
crates/workspace/src/lsp_status.rs
Normal file
|
@ -0,0 +1,137 @@
|
|||
use crate::{ItemViewHandle, Settings, StatusItemView};
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
action, elements::*, platform::CursorStyle, Entity, MutableAppContext, RenderContext, View,
|
||||
ViewContext,
|
||||
};
|
||||
use language::{LanguageRegistry, LanguageServerBinaryStatus};
|
||||
use postage::watch;
|
||||
use std::sync::Arc;
|
||||
|
||||
action!(DismissErrorMessage);
|
||||
|
||||
pub struct LspStatus {
|
||||
settings_rx: watch::Receiver<Settings>,
|
||||
checking_for_update: Vec<String>,
|
||||
downloading: Vec<String>,
|
||||
failed: Vec<String>,
|
||||
}
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(LspStatus::dismiss_error_message);
|
||||
}
|
||||
|
||||
impl LspStatus {
|
||||
pub fn new(
|
||||
languages: Arc<LanguageRegistry>,
|
||||
settings_rx: watch::Receiver<Settings>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let mut status_events = languages.language_server_binary_statuses();
|
||||
cx.spawn_weak(|this, mut cx| async move {
|
||||
while let Some((language, event)) = status_events.next().await {
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
for vector in [
|
||||
&mut this.checking_for_update,
|
||||
&mut this.downloading,
|
||||
&mut this.failed,
|
||||
] {
|
||||
vector.retain(|name| name != language.name());
|
||||
}
|
||||
|
||||
match event {
|
||||
LanguageServerBinaryStatus::CheckingForUpdate => {
|
||||
this.checking_for_update.push(language.name().to_string());
|
||||
}
|
||||
LanguageServerBinaryStatus::Downloading => {
|
||||
this.downloading.push(language.name().to_string());
|
||||
}
|
||||
LanguageServerBinaryStatus::Failed => {
|
||||
this.failed.push(language.name().to_string());
|
||||
}
|
||||
LanguageServerBinaryStatus::Downloaded
|
||||
| LanguageServerBinaryStatus::Cached => {}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
});
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
Self {
|
||||
settings_rx,
|
||||
checking_for_update: Default::default(),
|
||||
downloading: Default::default(),
|
||||
failed: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn dismiss_error_message(&mut self, _: &DismissErrorMessage, cx: &mut ViewContext<Self>) {
|
||||
self.failed.clear();
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for LspStatus {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for LspStatus {
|
||||
fn ui_name() -> &'static str {
|
||||
"LspStatus"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let theme = &self.settings_rx.borrow().theme;
|
||||
if !self.downloading.is_empty() {
|
||||
Label::new(
|
||||
format!(
|
||||
"Downloading {} language server{}...",
|
||||
self.downloading.join(", "),
|
||||
if self.downloading.len() > 1 { "s" } else { "" }
|
||||
),
|
||||
theme.workspace.status_bar.lsp_message.clone(),
|
||||
)
|
||||
.boxed()
|
||||
} else if !self.checking_for_update.is_empty() {
|
||||
Label::new(
|
||||
format!(
|
||||
"Checking for updates to {} language server{}...",
|
||||
self.checking_for_update.join(", "),
|
||||
if self.checking_for_update.len() > 1 {
|
||||
"s"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
),
|
||||
theme.workspace.status_bar.lsp_message.clone(),
|
||||
)
|
||||
.boxed()
|
||||
} else if !self.failed.is_empty() {
|
||||
MouseEventHandler::new::<Self, _, _>(0, cx, |_, _| {
|
||||
Label::new(
|
||||
format!(
|
||||
"Failed to download {} language server{}. Click to dismiss.",
|
||||
self.failed.join(", "),
|
||||
if self.failed.len() > 1 { "s" } else { "" }
|
||||
),
|
||||
theme.workspace.status_bar.lsp_message.clone(),
|
||||
)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(|cx| cx.dispatch_action(DismissErrorMessage))
|
||||
.boxed()
|
||||
} else {
|
||||
Empty::new().boxed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StatusItemView for LspStatus {
|
||||
fn set_active_pane_item(&mut self, _: Option<&dyn ItemViewHandle>, _: &mut ViewContext<Self>) {}
|
||||
}
|
|
@ -42,17 +42,21 @@ impl View for StatusBar {
|
|||
fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
|
||||
let theme = &self.settings.borrow().theme.workspace.status_bar;
|
||||
Flex::row()
|
||||
.with_children(
|
||||
self.left_items
|
||||
.iter()
|
||||
.map(|i| ChildView::new(i.as_ref()).aligned().boxed()),
|
||||
)
|
||||
.with_children(self.left_items.iter().map(|i| {
|
||||
ChildView::new(i.as_ref())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_margin_right(theme.item_spacing)
|
||||
.boxed()
|
||||
}))
|
||||
.with_child(Empty::new().flexible(1., true).boxed())
|
||||
.with_children(
|
||||
self.right_items
|
||||
.iter()
|
||||
.map(|i| ChildView::new(i.as_ref()).aligned().boxed()),
|
||||
)
|
||||
.with_children(self.right_items.iter().map(|i| {
|
||||
ChildView::new(i.as_ref())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_margin_left(theme.item_spacing)
|
||||
.boxed()
|
||||
}))
|
||||
.contained()
|
||||
.with_style(theme.container)
|
||||
.constrained()
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
pub mod lsp_status;
|
||||
pub mod menu;
|
||||
pub mod pane;
|
||||
pub mod pane_group;
|
||||
|
|
|
@ -55,6 +55,7 @@ theme_selector = { path = "../theme_selector" }
|
|||
util = { path = "../util" }
|
||||
workspace = { path = "../workspace" }
|
||||
anyhow = "1.0.38"
|
||||
async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
|
||||
async-recursion = "0.3"
|
||||
async-trait = "0.1"
|
||||
crossbeam-channel = "0.5.0"
|
||||
|
|
|
@ -77,9 +77,10 @@ border = { width = 1, color = "$border.0", left = true }
|
|||
[workspace.status_bar]
|
||||
padding = { left = 6, right = 6 }
|
||||
height = 24
|
||||
item_spacing = 24
|
||||
item_spacing = 8
|
||||
cursor_position = "$text.2"
|
||||
diagnostic_message = "$text.2"
|
||||
lsp_message = "$text.2"
|
||||
|
||||
[workspace.toolbar]
|
||||
height = 44
|
||||
|
@ -188,7 +189,7 @@ corner_radius = 6
|
|||
|
||||
[project_panel]
|
||||
extends = "$panel"
|
||||
padding.top = 6 # ($workspace.tab.height - $project_panel.entry.height) / 2
|
||||
padding.top = 6 # ($workspace.tab.height - $project_panel.entry.height) / 2
|
||||
|
||||
[project_panel.entry]
|
||||
text = "$text.1"
|
||||
|
|
|
@ -11,6 +11,5 @@ brackets = [
|
|||
]
|
||||
|
||||
[language_server]
|
||||
binary = "rust-analyzer"
|
||||
disk_based_diagnostic_sources = ["rustc"]
|
||||
disk_based_diagnostics_progress_token = "rustAnalyzer/cargo check"
|
||||
|
|
|
@ -1,17 +1,140 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use client::http::{self, HttpClient, Method};
|
||||
use futures::{future::BoxFuture, FutureExt, StreamExt};
|
||||
pub use language::*;
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use rust_embed::RustEmbed;
|
||||
use std::borrow::Cow;
|
||||
use std::{str, sync::Arc};
|
||||
use serde::Deserialize;
|
||||
use smol::fs::{self, File};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
env::consts,
|
||||
path::{Path, PathBuf},
|
||||
str,
|
||||
sync::Arc,
|
||||
};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "languages"]
|
||||
struct LanguageDir;
|
||||
|
||||
struct RustPostProcessor;
|
||||
struct RustLsp;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct GithubRelease {
|
||||
name: String,
|
||||
assets: Vec<GithubReleaseAsset>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct GithubReleaseAsset {
|
||||
name: String,
|
||||
browser_download_url: http::Url,
|
||||
}
|
||||
|
||||
impl LspExt for RustLsp {
|
||||
fn fetch_latest_server_version(
|
||||
&self,
|
||||
http: Arc<dyn HttpClient>,
|
||||
) -> BoxFuture<'static, Result<LspBinaryVersion>> {
|
||||
async move {
|
||||
let release = http
|
||||
.send(
|
||||
surf::RequestBuilder::new(
|
||||
Method::Get,
|
||||
http::Url::parse(
|
||||
"https://api.github.com/repos/rust-analyzer/rust-analyzer/releases/latest",
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.middleware(surf::middleware::Redirect::default())
|
||||
.build(),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| anyhow!("error fetching latest release: {}", err))?
|
||||
.body_json::<GithubRelease>()
|
||||
.await
|
||||
.map_err(|err| anyhow!("error parsing latest release: {}", err))?;
|
||||
let asset_name = format!("rust-analyzer-{}-apple-darwin.gz", consts::ARCH);
|
||||
let asset = release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|asset| asset.name == asset_name)
|
||||
.ok_or_else(|| anyhow!("no release found matching {:?}", asset_name))?;
|
||||
Ok(LspBinaryVersion {
|
||||
name: release.name,
|
||||
url: asset.browser_download_url.clone(),
|
||||
})
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn fetch_server_binary(
|
||||
&self,
|
||||
version: LspBinaryVersion,
|
||||
http: Arc<dyn HttpClient>,
|
||||
download_dir: Arc<Path>,
|
||||
) -> BoxFuture<'static, Result<PathBuf>> {
|
||||
async move {
|
||||
let destination_dir_path = download_dir.join("rust-analyzer");
|
||||
fs::create_dir_all(&destination_dir_path).await?;
|
||||
let destination_path =
|
||||
destination_dir_path.join(format!("rust-analyzer-{}", version.name));
|
||||
|
||||
if fs::metadata(&destination_path).await.is_err() {
|
||||
let response = http
|
||||
.send(
|
||||
surf::RequestBuilder::new(Method::Get, version.url)
|
||||
.middleware(surf::middleware::Redirect::default())
|
||||
.build(),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| anyhow!("error downloading release: {}", err))?;
|
||||
let decompressed_bytes = GzipDecoder::new(response);
|
||||
let mut file = File::create(&destination_path).await?;
|
||||
futures::io::copy(decompressed_bytes, &mut file).await?;
|
||||
fs::set_permissions(
|
||||
&destination_path,
|
||||
<fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(mut entries) = fs::read_dir(&destination_dir_path).await.log_err() {
|
||||
while let Some(entry) = entries.next().await {
|
||||
if let Some(entry) = entry.log_err() {
|
||||
let entry_path = entry.path();
|
||||
if entry_path.as_path() != destination_path {
|
||||
fs::remove_file(&entry_path).await.log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(destination_path)
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn cached_server_binary(&self, download_dir: Arc<Path>) -> BoxFuture<'static, Option<PathBuf>> {
|
||||
async move {
|
||||
let destination_dir_path = download_dir.join("rust-analyzer");
|
||||
fs::create_dir_all(&destination_dir_path).await?;
|
||||
|
||||
let mut last = None;
|
||||
let mut entries = fs::read_dir(&destination_dir_path).await?;
|
||||
while let Some(entry) = entries.next().await {
|
||||
last = Some(entry?.path());
|
||||
}
|
||||
last.ok_or_else(|| anyhow!("no cached binary"))
|
||||
}
|
||||
.log_err()
|
||||
.boxed()
|
||||
}
|
||||
|
||||
impl LspPostProcessor for RustPostProcessor {
|
||||
fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) {
|
||||
lazy_static! {
|
||||
static ref REGEX: Regex = Regex::new("(?m)`([^`]+)\n`$").unwrap();
|
||||
|
@ -113,7 +236,12 @@ impl LspPostProcessor for RustPostProcessor {
|
|||
}
|
||||
|
||||
pub fn build_language_registry() -> LanguageRegistry {
|
||||
let mut languages = LanguageRegistry::default();
|
||||
let mut languages = LanguageRegistry::new();
|
||||
languages.set_language_server_download_dir(
|
||||
dirs::home_dir()
|
||||
.expect("failed to determine home directory")
|
||||
.join(".zed"),
|
||||
);
|
||||
languages.add(Arc::new(rust()));
|
||||
languages.add(Arc::new(markdown()));
|
||||
languages
|
||||
|
@ -131,7 +259,7 @@ fn rust() -> Language {
|
|||
.unwrap()
|
||||
.with_outline_query(load_query("rust/outline.scm").as_ref())
|
||||
.unwrap()
|
||||
.with_lsp_post_processor(RustPostProcessor)
|
||||
.with_lsp_ext(RustLsp)
|
||||
}
|
||||
|
||||
fn markdown() -> Language {
|
||||
|
@ -153,7 +281,7 @@ fn load_query(path: &str) -> Cow<'static, str> {
|
|||
mod tests {
|
||||
use super::*;
|
||||
use gpui::color::Color;
|
||||
use language::LspPostProcessor;
|
||||
use language::LspExt;
|
||||
use theme::SyntaxTheme;
|
||||
|
||||
#[test]
|
||||
|
@ -180,7 +308,7 @@ mod tests {
|
|||
},
|
||||
],
|
||||
};
|
||||
RustPostProcessor.process_diagnostics(&mut params);
|
||||
RustLsp.process_diagnostics(&mut params);
|
||||
|
||||
assert_eq!(params.diagnostics[0].message, "use of moved value `a`");
|
||||
|
||||
|
|
|
@ -26,14 +26,17 @@ pub fn test_app_state(cx: &mut MutableAppContext) -> Arc<AppState> {
|
|||
let client = Client::new(http.clone());
|
||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
|
||||
let mut languages = LanguageRegistry::new();
|
||||
languages.add(Arc::new(language::Language::new(
|
||||
language::LanguageConfig {
|
||||
name: "Rust".to_string(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
)));
|
||||
languages.add(
|
||||
Arc::new(language::Language::new(
|
||||
language::LanguageConfig {
|
||||
name: "Rust".to_string(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
)),
|
||||
|
||||
);
|
||||
Arc::new(AppState {
|
||||
settings_tx: Arc::new(Mutex::new(settings_tx)),
|
||||
settings,
|
||||
|
|
|
@ -43,6 +43,8 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
|
|||
}
|
||||
});
|
||||
|
||||
workspace::lsp_status::init(cx);
|
||||
|
||||
cx.add_bindings(vec![
|
||||
Binding::new("cmd-=", AdjustBufferFontSize(1.), None),
|
||||
Binding::new("cmd--", AdjustBufferFontSize(-1.), None),
|
||||
|
@ -97,11 +99,19 @@ pub fn build_workspace(
|
|||
cx,
|
||||
)
|
||||
});
|
||||
let lsp_status = cx.add_view(|cx| {
|
||||
workspace::lsp_status::LspStatus::new(
|
||||
app_state.languages.clone(),
|
||||
app_state.settings.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let cursor_position =
|
||||
cx.add_view(|_| editor::items::CursorPosition::new(app_state.settings.clone()));
|
||||
workspace.status_bar().update(cx, |status_bar, cx| {
|
||||
status_bar.add_left_item(diagnostic_summary, cx);
|
||||
status_bar.add_left_item(diagnostic_message, cx);
|
||||
status_bar.add_left_item(lsp_status, cx);
|
||||
status_bar.add_right_item(cursor_position, cx);
|
||||
});
|
||||
|
||||
|
|
|
@ -21,9 +21,6 @@ cargo build --release --target aarch64-apple-darwin
|
|||
# Replace the bundle's binary with a "fat binary" that combines the two architecture-specific binaries
|
||||
lipo -create target/x86_64-apple-darwin/release/Zed target/aarch64-apple-darwin/release/Zed -output target/x86_64-apple-darwin/release/bundle/osx/Zed.app/Contents/MacOS/zed
|
||||
|
||||
# Bundle rust-analyzer
|
||||
cp vendor/bin/rust-analyzer target/x86_64-apple-darwin/release/bundle/osx/Zed.app/Contents/Resources/
|
||||
|
||||
# Sign the app bundle with an ad-hoc signature so it runs on the M1. We need a real certificate but this works for now.
|
||||
if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTARIZATION_USERNAME && -n $APPLE_NOTARIZATION_PASSWORD ]]; then
|
||||
echo "Signing bundle with Apple-issued certificate"
|
||||
|
@ -34,7 +31,6 @@ if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTAR
|
|||
security import /tmp/zed-certificate.p12 -k zed.keychain -P $MACOS_CERTIFICATE_PASSWORD -T /usr/bin/codesign
|
||||
rm /tmp/zed-certificate.p12
|
||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $MACOS_CERTIFICATE_PASSWORD zed.keychain
|
||||
/usr/bin/codesign --force --deep --timestamp --options runtime --sign "Zed Industries, Inc." target/x86_64-apple-darwin/release/bundle/osx/Zed.app/Contents/Resources/rust-analyzer -v
|
||||
/usr/bin/codesign --force --deep --timestamp --options runtime --sign "Zed Industries, Inc." target/x86_64-apple-darwin/release/bundle/osx/Zed.app -v
|
||||
security default-keychain -s login.keychain
|
||||
else
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
export RUST_ANALYZER_URL="https://github.com/rust-analyzer/rust-analyzer/releases/download/2022-01-24/"
|
||||
|
||||
function download {
|
||||
local filename="rust-analyzer-$1"
|
||||
curl -L $RUST_ANALYZER_URL/$filename.gz | gunzip > vendor/bin/$filename
|
||||
chmod +x vendor/bin/$filename
|
||||
}
|
||||
|
||||
mkdir -p vendor/bin
|
||||
download "x86_64-apple-darwin"
|
||||
download "aarch64-apple-darwin"
|
||||
|
||||
cd vendor/bin
|
||||
lipo -create rust-analyzer-* -output rust-analyzer
|
||||
rm rust-analyzer-*
|
Loading…
Reference in a new issue