Merge pull request #459 from zed-industries/spurious-macro-errors

Download language servers dynamically on startup
This commit is contained in:
Nathan Sobo 2022-02-21 17:08:50 -08:00 committed by GitHub
commit c752383042
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 743 additions and 362 deletions

View file

@ -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
View file

@ -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",

View file

@ -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

View file

@ -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()

View file

@ -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"] }

View file

@ -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)
}

View file

@ -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!(

View file

@ -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 {

View file

@ -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()

View file

@ -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(

View file

@ -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)]

View file

@ -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"] }

View 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>) {}
}

View file

@ -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()

View file

@ -1,3 +1,4 @@
pub mod lsp_status;
pub mod menu;
pub mod pane;
pub mod pane_group;

View file

@ -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"

View file

@ -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"

View file

@ -11,6 +11,5 @@ brackets = [
]
[language_server]
binary = "rust-analyzer"
disk_based_diagnostic_sources = ["rustc"]
disk_based_diagnostics_progress_token = "rustAnalyzer/cargo check"

View file

@ -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`");

View file

@ -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,

View file

@ -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);
});

View file

@ -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

View file

@ -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-*