Fix issues with extension API that come up when moving Svelte into an extension (#9611)

We're doing it. Svelte support is moving into an extension. This PR
fixes some issues that came up along the way.

Notes

* extensions need to be able to retrieve the path the `node` binary
installed by Zed
* previously we were silently swallowing any errors that occurred while
loading a grammar
* npm commands ran by extensions weren't run in the right directory
* Tree-sitter's WASM stdlib didn't support a C function (`strncmp`)
needed by the Svelte parser's external scanner
* the way that LSP installation status was reported was unnecessarily
complex

Release Notes:

- Removed built-in support for the Svelte and Gleam languages, because
full support for those languages is now available via extensions. These
extensions will be suggested for download when you open a `.svelte` or
`.gleam` file.

---------

Co-authored-by: Marshall <marshall@zed.dev>
This commit is contained in:
Max Brunsfeld 2024-03-22 17:29:06 -07:00 committed by GitHub
parent 4459eacc98
commit 6ebe599c98
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
70 changed files with 1278 additions and 1223 deletions

410
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,4 @@
[workspace]
members = [
"crates/activity_indicator",
@ -97,6 +98,7 @@ members = [
"extensions/gleam",
"extensions/uiua",
"extensions/svelte",
"tooling/xtask",
]
@ -210,7 +212,7 @@ bitflags = "2.4.2"
blade-graphics = { git = "https://github.com/kvark/blade", rev = "61cbd6b2c224791d52b150fe535cee665cc91bb2" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "61cbd6b2c224791d52b150fe535cee665cc91bb2" }
blade-rwh = { package = "raw-window-handle", version = "0.5" }
cap-std = "2.0"
cap-std = "3.0"
chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4.4", features = ["derive"] }
clickhouse = { version = "0.11.6" }
@ -293,7 +295,6 @@ tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir"
tree-sitter-elm = { git = "https://github.com/elm-tooling/tree-sitter-elm", rev = "692c50c0b961364c40299e73c1306aecb5d20f40" }
tree-sitter-embedded-template = "0.20.0"
tree-sitter-erlang = "0.4.0"
tree-sitter-gleam = { git = "https://github.com/gleam-lang/tree-sitter-gleam", rev = "58b7cac8fc14c92b0677c542610d8738c373fa81" }
tree-sitter-glsl = { git = "https://github.com/theHamsta/tree-sitter-glsl", rev = "2a56fb7bc8bb03a1892b4741279dd0a8758b7fb3" }
tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" }
tree-sitter-gomod = { git = "https://github.com/camdencheek/tree-sitter-go-mod" }
@ -320,7 +321,6 @@ tree-sitter-regex = "0.20.0"
tree-sitter-ruby = "0.20.0"
tree-sitter-rust = "0.20.3"
tree-sitter-scheme = { git = "https://github.com/6cdh/tree-sitter-scheme", rev = "af0fd1fa452cb2562dc7b5c8a8c55551c39273b9" }
tree-sitter-svelte = { git = "https://github.com/Himujjal/tree-sitter-svelte", rev = "bd60db7d3d06f89b6ec3b287c9a6e9190b5564bd" }
tree-sitter-toml = { git = "https://github.com/tree-sitter/tree-sitter-toml", rev = "342d9be207c2dba869b9967124c679b5e6fd0ebe" }
tree-sitter-typescript = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "5d20856f34315b068c41edaee2ac8a100081d259" }
tree-sitter-vue = { git = "https://github.com/zed-industries/tree-sitter-vue", rev = "6608d9d60c386f19d80af7d8132322fa11199c42" }
@ -330,18 +330,18 @@ unindent = "0.1.7"
unicase = "2.6"
url = "2.2"
uuid = { version = "1.1.2", features = ["v4"] }
wasmparser = "0.121"
wasm-encoder = "0.41"
wasmtime = { version = "18.0", default-features = false, features = [
wasmparser = "0.201"
wasm-encoder = "0.201"
wasmtime = { version = "19.0.0", default-features = false, features = [
"async",
"demangle",
"runtime",
"cranelift",
"component-model",
] }
wasmtime-wasi = "18.0"
wasmtime-wasi = "19.0.0"
which = "6.0.0"
wit-component = "0.20"
wit-component = "0.201"
sys-locale = "0.3.1"
[workspace.dependencies.windows]
@ -375,7 +375,7 @@ features = [
]
[patch.crates-io]
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "4294e59279205f503eb14348dd5128bd5910c8fb" }
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "05079ae3d1bc44bedc4594eef925b36ba5e317a2" }
# Workaround for a broken nightly build of gpui: See #7644 and revisit once 0.5.3 is released.
pathfinder_simd = { git = "https://github.com/servo/pathfinder.git", rev = "30419d07660dc11a21e42ef4a7fa329600cff152" }
@ -389,6 +389,7 @@ cranelift-codegen = { opt-level = 3 }
rustybuzz = { opt-level = 3 }
ttf-parser = { opt-level = 3 }
wasmtime-cranelift = { opt-level = 3 }
wasmtime = { opt-level = 3 }
[profile.release]
debug = "limited"

View File

@ -205,7 +205,7 @@ impl ActivityIndicator {
}
LanguageServerBinaryStatus::Downloading => downloading.push(status.name.0.as_ref()),
LanguageServerBinaryStatus::Failed { .. } => failed.push(status.name.0.as_ref()),
LanguageServerBinaryStatus::Downloaded | LanguageServerBinaryStatus::Cached => {}
LanguageServerBinaryStatus::None => {}
}
}

View File

@ -17,7 +17,7 @@ use rpc::{
};
use settings::Settings;
use std::{mem, sync::Arc, time::Duration};
use util::{async_maybe, maybe, ResultExt};
use util::{maybe, ResultExt};
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
@ -227,7 +227,7 @@ impl ChannelStore {
_watch_connection_status: watch_connection_status,
disconnect_channel_buffers_task: None,
_update_channels: cx.spawn(|this, mut cx| async move {
async_maybe!({
maybe!(async move {
while let Some(update_channels) = update_channels_rx.next().await {
if let Some(this) = this.upgrade() {
let update_task = this.update(&mut cx, |this, cx| {

View File

@ -1228,25 +1228,21 @@ async fn create_room(
) -> Result<()> {
let live_kit_room = nanoid::nanoid!(30);
let live_kit_connection_info = {
let live_kit_room = live_kit_room.clone();
let live_kit_connection_info = util::maybe!(async {
let live_kit = session.live_kit_client.as_ref();
let live_kit = live_kit?;
let user_id = session.user_id().to_string();
util::async_maybe!({
let live_kit = live_kit?;
let token = live_kit
.room_token(&live_kit_room, &user_id.to_string())
.trace_err()?;
let token = live_kit
.room_token(&live_kit_room, &user_id.to_string())
.trace_err()?;
Some(proto::LiveKitConnectionInfo {
server_url: live_kit.url().into(),
token,
can_publish: true,
})
Some(proto::LiveKitConnectionInfo {
server_url: live_kit.url().into(),
token,
can_publish: true,
})
}
})
.await;
let room = session

View File

@ -29,8 +29,7 @@ use std::{
sync::Arc,
};
use util::{
async_maybe, fs::remove_matching, github::latest_github_release, http::HttpClient, paths,
ResultExt,
fs::remove_matching, github::latest_github_release, http::HttpClient, maybe, paths, ResultExt,
};
actions!(
@ -1006,7 +1005,7 @@ async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
e @ Err(..) => {
e.log_err();
// Fetch a cached binary, if it exists
async_maybe!({
maybe!(async {
let mut last_version_dir = None;
let mut entries = fs::read_dir(paths::COPILOT_DIR.as_path()).await?;
while let Some(entry) = entries.next().await {

View File

@ -20,7 +20,7 @@ use sqlez_macros::sql;
use std::future::Future;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use util::{async_maybe, ResultExt};
use util::{maybe, ResultExt};
const CONNECTION_INITIALIZE_QUERY: &str = sql!(
PRAGMA foreign_keys=TRUE;
@ -57,7 +57,7 @@ pub async fn open_db<M: Migrator + 'static>(
let release_channel_name = release_channel.dev_name();
let main_db_dir = db_dir.join(Path::new(&format!("0-{}", release_channel_name)));
let connection = async_maybe!({
let connection = maybe!(async {
smol::fs::create_dir_all(&main_db_dir)
.await
.context("Could not create db directory")

View File

@ -85,10 +85,7 @@ impl ExtensionBuilder {
let cargo_toml_path = extension_dir.join("Cargo.toml");
if extension_manifest.lib.kind == Some(ExtensionLibraryKind::Rust)
|| fs::metadata(&cargo_toml_path)
.ok()
.map(|metadata| metadata.is_file())
.unwrap_or(false)
|| fs::metadata(&cargo_toml_path).map_or(false, |stat| stat.is_file())
{
log::info!("compiling Rust extension {}", extension_dir.display());
self.compile_rust_extension(extension_dir, options)

View File

@ -11,7 +11,7 @@ use std::{
pin::Pin,
sync::Arc,
};
use wasmtime_wasi::preview2::WasiView as _;
use wasmtime_wasi::WasiView as _;
pub struct ExtensionLspAdapter {
pub(crate) extension: WasmExtension,

View File

@ -39,6 +39,7 @@ use theme::{ThemeRegistry, ThemeSettings};
use url::Url;
use util::{
http::{AsyncBody, HttpClient, HttpClientWithUrl},
maybe,
paths::EXTENSIONS_DIR,
ResultExt,
};
@ -108,6 +109,7 @@ pub enum Event {
ExtensionsUpdated,
StartedReloading,
ExtensionInstalled(Arc<str>),
ExtensionFailedToLoad(Arc<str>),
}
impl EventEmitter<Event> for ExtensionStore {}
@ -886,41 +888,38 @@ impl ExtensionStore {
continue;
};
let mut path = root_dir.clone();
path.extend([extension.manifest.id.as_ref(), "extension.wasm"]);
let Some(mut wasm_file) = fs
.open_sync(&path)
.await
.context("failed to open wasm file")
.log_err()
else {
continue;
};
let wasm_extension = maybe!(async {
let mut path = root_dir.clone();
path.extend([extension.manifest.clone().id.as_ref(), "extension.wasm"]);
let mut wasm_file = fs
.open_sync(&path)
.await
.context("failed to open wasm file")?;
let mut wasm_bytes = Vec::new();
if wasm_file
.read_to_end(&mut wasm_bytes)
.context("failed to read wasm")
.log_err()
.is_none()
{
continue;
let mut wasm_bytes = Vec::new();
wasm_file
.read_to_end(&mut wasm_bytes)
.context("failed to read wasm")?;
wasm_host
.load_extension(
wasm_bytes,
extension.manifest.clone().clone(),
cx.background_executor().clone(),
)
.await
.context("failed to load wasm extension")
})
.await;
if let Some(wasm_extension) = wasm_extension.log_err() {
wasm_extensions.push((extension.manifest.clone(), wasm_extension));
} else {
this.update(&mut cx, |_, cx| {
cx.emit(Event::ExtensionFailedToLoad(extension.manifest.id.clone()))
})
.ok();
}
let Some(wasm_extension) = wasm_host
.load_extension(
wasm_bytes,
extension.manifest.clone(),
cx.background_executor().clone(),
)
.await
.context("failed to load wasm extension")
.log_err()
else {
continue;
};
wasm_extensions.push((extension.manifest.clone(), wasm_extension));
}
this.update(&mut cx, |this, cx| {

View File

@ -1,6 +1,7 @@
use crate::{
ExtensionIndex, ExtensionIndexEntry, ExtensionIndexLanguageEntry, ExtensionIndexThemeEntry,
ExtensionManifest, ExtensionStore, GrammarManifestEntry, RELOAD_DEBOUNCE_DURATION,
Event, ExtensionIndex, ExtensionIndexEntry, ExtensionIndexLanguageEntry,
ExtensionIndexThemeEntry, ExtensionManifest, ExtensionStore, GrammarManifestEntry,
RELOAD_DEBOUNCE_DURATION,
};
use async_compression::futures::bufread::GzipEncoder;
use collections::BTreeMap;
@ -558,6 +559,15 @@ async fn test_extension_store_with_gleam_extension(cx: &mut TestAppContext) {
}
});
extension_store.update(cx, |_, cx| {
cx.subscribe(&extension_store, |_, _, event, _| {
if matches!(event, Event::ExtensionFailedToLoad(_)) {
panic!("extension failed to load");
}
})
.detach();
});
extension_store
.update(cx, |store, cx| {
store.install_dev_extension(gleam_extension_dir.clone(), cx)
@ -602,7 +612,7 @@ async fn test_extension_store_with_gleam_extension(cx: &mut TestAppContext) {
),
(
LanguageServerName("gleam".into()),
LanguageServerBinaryStatus::Downloaded
LanguageServerBinaryStatus::None
)
]
);

View File

@ -1,8 +1,7 @@
pub(crate) mod wit;
use crate::ExtensionManifest;
use anyhow::{anyhow, bail, Context as _, Result};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use async_trait::async_trait;
use fs::{normalize_path, Fs};
use futures::{
channel::{
@ -10,42 +9,28 @@ use futures::{
oneshot,
},
future::BoxFuture,
io::BufReader,
Future, FutureExt, StreamExt as _,
};
use gpui::BackgroundExecutor;
use language::{LanguageRegistry, LanguageServerBinaryStatus, LspAdapterDelegate};
use language::LanguageRegistry;
use node_runtime::NodeRuntime;
use std::{
env,
path::{Path, PathBuf},
sync::{Arc, OnceLock},
};
use util::{http::HttpClient, SemanticVersion};
use wasmtime::{
component::{Component, Linker, Resource, ResourceTable},
component::{Component, ResourceTable},
Engine, Store,
};
use wasmtime_wasi::preview2::{self as wasi, WasiCtx};
pub mod wit {
wasmtime::component::bindgen!({
async: true,
path: "../extension_api/wit",
with: {
"worktree": super::ExtensionWorktree,
},
});
}
pub type ExtensionWorktree = Arc<dyn LspAdapterDelegate>;
use wasmtime_wasi as wasi;
use wit::Extension;
pub(crate) struct WasmHost {
engine: Engine,
linker: Arc<wasmtime::component::Linker<WasmState>>,
http_client: Arc<dyn HttpClient>,
node_runtime: Arc<dyn NodeRuntime>,
language_registry: Arc<LanguageRegistry>,
pub(crate) language_registry: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>,
pub(crate) work_dir: PathBuf,
}
@ -60,17 +45,27 @@ pub struct WasmExtension {
pub(crate) struct WasmState {
manifest: Arc<ExtensionManifest>,
table: ResourceTable,
pub(crate) table: ResourceTable,
ctx: wasi::WasiCtx,
host: Arc<WasmHost>,
pub(crate) host: Arc<WasmHost>,
}
type ExtensionCall = Box<
dyn Send
+ for<'a> FnOnce(&'a mut wit::Extension, &'a mut Store<WasmState>) -> BoxFuture<'a, ()>,
dyn Send + for<'a> FnOnce(&'a mut Extension, &'a mut Store<WasmState>) -> BoxFuture<'a, ()>,
>;
static WASM_ENGINE: OnceLock<wasmtime::Engine> = OnceLock::new();
fn wasm_engine() -> wasmtime::Engine {
static WASM_ENGINE: OnceLock<wasmtime::Engine> = OnceLock::new();
WASM_ENGINE
.get_or_init(|| {
let mut config = wasmtime::Config::new();
config.wasm_component_model(true);
config.async_support(true);
wasmtime::Engine::new(&config).unwrap()
})
.clone()
}
impl WasmHost {
pub fn new(
@ -80,20 +75,8 @@ impl WasmHost {
language_registry: Arc<LanguageRegistry>,
work_dir: PathBuf,
) -> Arc<Self> {
let engine = WASM_ENGINE
.get_or_init(|| {
let mut config = wasmtime::Config::new();
config.wasm_component_model(true);
config.async_support(true);
wasmtime::Engine::new(&config).unwrap()
})
.clone();
let mut linker = Linker::new(&engine);
wasi::command::add_to_linker(&mut linker).unwrap();
wit::Extension::add_to_linker(&mut linker, wasi_view).unwrap();
Arc::new(Self {
engine,
linker: Arc::new(linker),
engine: wasm_engine(),
fs,
work_dir,
http_client,
@ -144,9 +127,8 @@ impl WasmHost {
);
let (mut extension, instance) =
wit::Extension::instantiate_async(&mut store, &component, &this.linker)
.await
.context("failed to instantiate wasm extension")?;
Extension::instantiate_async(&mut store, zed_api_version, &component).await?;
extension
.call_init_extension(&mut store)
.await
@ -170,7 +152,7 @@ impl WasmHost {
}
}
async fn build_wasi_ctx(&self, manifest: &Arc<ExtensionManifest>) -> Result<WasiCtx> {
async fn build_wasi_ctx(&self, manifest: &Arc<ExtensionManifest>) -> Result<wasi::WasiCtx> {
use cap_std::{ambient_authority, fs::Dir};
let extension_work_dir = self.work_dir.join(manifest.id.as_ref());
@ -232,7 +214,7 @@ impl WasmExtension {
T: 'static + Send,
Fn: 'static
+ Send
+ for<'a> FnOnce(&'a mut wit::Extension, &'a mut Store<WasmState>) -> BoxFuture<'a, T>,
+ for<'a> FnOnce(&'a mut Extension, &'a mut Store<WasmState>) -> BoxFuture<'a, T>,
{
let (return_tx, return_rx) = oneshot::channel();
self.tx
@ -249,279 +231,10 @@ impl WasmExtension {
}
}
#[async_trait]
impl wit::HostWorktree for WasmState {
async fn read_text_file(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
path: String,
) -> wasmtime::Result<Result<String, String>> {
let delegate = self.table.get(&delegate)?;
Ok(delegate
.read_text_file(path.into())
.await
.map_err(|error| error.to_string()))
impl WasmState {
fn work_dir(&self) -> PathBuf {
self.host.work_dir.join(self.manifest.id.as_ref())
}
async fn shell_env(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
) -> wasmtime::Result<wit::EnvVars> {
let delegate = self.table.get(&delegate)?;
Ok(delegate.shell_env().await.into_iter().collect())
}
async fn which(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
binary_name: String,
) -> wasmtime::Result<Option<String>> {
let delegate = self.table.get(&delegate)?;
Ok(delegate
.which(binary_name.as_ref())
.await
.map(|path| path.to_string_lossy().to_string()))
}
fn drop(&mut self, _worktree: Resource<wit::Worktree>) -> Result<()> {
// we only ever hand out borrows of worktrees
Ok(())
}
}
#[async_trait]
impl wit::ExtensionImports for WasmState {
async fn npm_package_latest_version(
&mut self,
package_name: String,
) -> wasmtime::Result<Result<String, String>> {
async fn inner(this: &mut WasmState, package_name: String) -> anyhow::Result<String> {
this.host
.node_runtime
.npm_package_latest_version(&package_name)
.await
}
Ok(inner(self, package_name)
.await
.map_err(|err| err.to_string()))
}
async fn npm_package_installed_version(
&mut self,
package_name: String,
) -> wasmtime::Result<Result<Option<String>, String>> {
async fn inner(
this: &mut WasmState,
package_name: String,
) -> anyhow::Result<Option<String>> {
this.host
.node_runtime
.npm_package_installed_version(&this.host.work_dir, &package_name)
.await
}
Ok(inner(self, package_name)
.await
.map_err(|err| err.to_string()))
}
async fn npm_install_package(
&mut self,
package_name: String,
version: String,
) -> wasmtime::Result<Result<(), String>> {
async fn inner(
this: &mut WasmState,
package_name: String,
version: String,
) -> anyhow::Result<()> {
this.host
.node_runtime
.npm_install_packages(&this.host.work_dir, &[(&package_name, &version)])
.await
}
Ok(inner(self, package_name, version)
.await
.map_err(|err| err.to_string()))
}
async fn latest_github_release(
&mut self,
repo: String,
options: wit::GithubReleaseOptions,
) -> wasmtime::Result<Result<wit::GithubRelease, String>> {
async fn inner(
this: &mut WasmState,
repo: String,
options: wit::GithubReleaseOptions,
) -> anyhow::Result<wit::GithubRelease> {
let release = util::github::latest_github_release(
&repo,
options.require_assets,
options.pre_release,
this.host.http_client.clone(),
)
.await?;
Ok(wit::GithubRelease {
version: release.tag_name,
assets: release
.assets
.into_iter()
.map(|asset| wit::GithubReleaseAsset {
name: asset.name,
download_url: asset.browser_download_url,
})
.collect(),
})
}
Ok(inner(self, repo, options)
.await
.map_err(|err| err.to_string()))
}
async fn current_platform(&mut self) -> Result<(wit::Os, wit::Architecture)> {
Ok((
match env::consts::OS {
"macos" => wit::Os::Mac,
"linux" => wit::Os::Linux,
"windows" => wit::Os::Windows,
_ => panic!("unsupported os"),
},
match env::consts::ARCH {
"aarch64" => wit::Architecture::Aarch64,
"x86" => wit::Architecture::X86,
"x86_64" => wit::Architecture::X8664,
_ => panic!("unsupported architecture"),
},
))
}
async fn set_language_server_installation_status(
&mut self,
server_name: String,
status: wit::LanguageServerInstallationStatus,
) -> wasmtime::Result<()> {
let status = match status {
wit::LanguageServerInstallationStatus::CheckingForUpdate => {
LanguageServerBinaryStatus::CheckingForUpdate
}
wit::LanguageServerInstallationStatus::Downloading => {
LanguageServerBinaryStatus::Downloading
}
wit::LanguageServerInstallationStatus::Downloaded => {
LanguageServerBinaryStatus::Downloaded
}
wit::LanguageServerInstallationStatus::Cached => LanguageServerBinaryStatus::Cached,
wit::LanguageServerInstallationStatus::Failed(error) => {
LanguageServerBinaryStatus::Failed { error }
}
};
self.host
.language_registry
.update_lsp_status(language::LanguageServerName(server_name.into()), status);
Ok(())
}
async fn download_file(
&mut self,
url: String,
path: String,
file_type: wit::DownloadedFileType,
) -> wasmtime::Result<Result<(), String>> {
let path = PathBuf::from(path);
async fn inner(
this: &mut WasmState,
url: String,
path: PathBuf,
file_type: wit::DownloadedFileType,
) -> anyhow::Result<()> {
let extension_work_dir = this.host.work_dir.join(this.manifest.id.as_ref());
this.host.fs.create_dir(&extension_work_dir).await?;
let destination_path = this
.host
.writeable_path_from_extension(&this.manifest.id, &path)?;
let mut response = this
.host
.http_client
.get(&url, Default::default(), true)
.await
.map_err(|err| anyhow!("error downloading release: {}", err))?;
if !response.status().is_success() {
Err(anyhow!(
"download failed with status {}",
response.status().to_string()
))?;
}
let body = BufReader::new(response.body_mut());
match file_type {
wit::DownloadedFileType::Uncompressed => {
futures::pin_mut!(body);
this.host
.fs
.create_file_with(&destination_path, body)
.await?;
}
wit::DownloadedFileType::Gzip => {
let body = GzipDecoder::new(body);
futures::pin_mut!(body);
this.host
.fs
.create_file_with(&destination_path, body)
.await?;
}
wit::DownloadedFileType::GzipTar => {
let body = GzipDecoder::new(body);
futures::pin_mut!(body);
this.host
.fs
.extract_tar_file(&destination_path, Archive::new(body))
.await?;
}
wit::DownloadedFileType::Zip => {
let file_name = destination_path
.file_name()
.ok_or_else(|| anyhow!("invalid download path"))?
.to_string_lossy();
let zip_filename = format!("{file_name}.zip");
let mut zip_path = destination_path.clone();
zip_path.set_file_name(zip_filename);
futures::pin_mut!(body);
this.host.fs.create_file_with(&zip_path, body).await?;
let unzip_status = std::process::Command::new("unzip")
.current_dir(&extension_work_dir)
.arg(&zip_path)
.output()?
.status;
if !unzip_status.success() {
Err(anyhow!("failed to unzip {} archive", path.display()))?;
}
}
}
Ok(())
}
Ok(inner(self, url, path, file_type)
.await
.map(|_| ())
.map_err(|err| err.to_string()))
}
}
fn wasi_view(state: &mut WasmState) -> &mut WasmState {
state
}
impl wasi::WasiView for WasmState {

View File

@ -0,0 +1,103 @@
mod v0_0_1;
mod v0_0_4;
use super::{wasm_engine, WasmState};
use anyhow::{Context, Result};
use language::LspAdapterDelegate;
use std::sync::Arc;
use util::SemanticVersion;
use wasmtime::{
component::{Component, Instance, Linker, Resource},
Store,
};
use v0_0_4 as latest;
pub use latest::{Command, LanguageServerConfig};
pub fn new_linker(
f: impl Fn(&mut Linker<WasmState>, fn(&mut WasmState) -> &mut WasmState) -> Result<()>,
) -> Linker<WasmState> {
let mut linker = Linker::new(&wasm_engine());
wasmtime_wasi::command::add_to_linker(&mut linker).unwrap();
f(&mut linker, wasi_view).unwrap();
linker
}
fn wasi_view(state: &mut WasmState) -> &mut WasmState {
state
}
pub enum Extension {
V004(v0_0_4::Extension),
V001(v0_0_1::Extension),
}
impl Extension {
pub async fn instantiate_async(
store: &mut Store<WasmState>,
version: SemanticVersion,
component: &Component,
) -> Result<(Self, Instance)> {
if version < latest::VERSION {
let (extension, instance) =
v0_0_1::Extension::instantiate_async(store, &component, v0_0_1::linker())
.await
.context("failed to instantiate wasm extension")?;
Ok((Self::V001(extension), instance))
} else {
let (extension, instance) =
v0_0_4::Extension::instantiate_async(store, &component, v0_0_4::linker())
.await
.context("failed to instantiate wasm extension")?;
Ok((Self::V004(extension), instance))
}
}
pub async fn call_init_extension(&self, store: &mut Store<WasmState>) -> Result<()> {
match self {
Extension::V004(ext) => ext.call_init_extension(store).await,
Extension::V001(ext) => ext.call_init_extension(store).await,
}
}
pub async fn call_language_server_command(
&self,
store: &mut Store<WasmState>,
config: &LanguageServerConfig,
resource: Resource<Arc<dyn LspAdapterDelegate>>,
) -> Result<Result<Command, String>> {
match self {
Extension::V004(ext) => {
ext.call_language_server_command(store, config, resource)
.await
}
Extension::V001(ext) => Ok(ext
.call_language_server_command(store, &config.clone().into(), resource)
.await?
.map(|command| command.into())),
}
}
pub async fn call_language_server_initialization_options(
&self,
store: &mut Store<WasmState>,
config: &LanguageServerConfig,
resource: Resource<Arc<dyn LspAdapterDelegate>>,
) -> Result<Result<Option<String>, String>> {
match self {
Extension::V004(ext) => {
ext.call_language_server_initialization_options(store, config, resource)
.await
}
Extension::V001(ext) => {
ext.call_language_server_initialization_options(
store,
&config.clone().into(),
resource,
)
.await
}
}
}
}

View File

@ -0,0 +1,210 @@
use super::latest;
use crate::wasm_host::WasmState;
use anyhow::Result;
use async_trait::async_trait;
use language::{LanguageServerBinaryStatus, LspAdapterDelegate};
use std::sync::{Arc, OnceLock};
use wasmtime::component::{Linker, Resource};
wasmtime::component::bindgen!({
async: true,
path: "../extension_api/wit/0.0.1",
with: {
"worktree": ExtensionWorktree,
},
});
pub type ExtensionWorktree = Arc<dyn LspAdapterDelegate>;
pub fn linker() -> &'static Linker<WasmState> {
static LINKER: OnceLock<Linker<WasmState>> = OnceLock::new();
LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker))
}
impl From<latest::Os> for Os {
fn from(value: latest::Os) -> Self {
match value {
latest::Os::Mac => Os::Mac,
latest::Os::Linux => Os::Linux,
latest::Os::Windows => Os::Windows,
}
}
}
impl From<latest::Architecture> for Architecture {
fn from(value: latest::Architecture) -> Self {
match value {
latest::Architecture::Aarch64 => Self::Aarch64,
latest::Architecture::X86 => Self::X86,
latest::Architecture::X8664 => Self::X8664,
}
}
}
impl From<latest::GithubRelease> for GithubRelease {
fn from(value: latest::GithubRelease) -> Self {
Self {
version: value.version,
assets: value.assets.into_iter().map(|asset| asset.into()).collect(),
}
}
}
impl From<latest::GithubReleaseAsset> for GithubReleaseAsset {
fn from(value: latest::GithubReleaseAsset) -> Self {
Self {
name: value.name,
download_url: value.download_url,
}
}
}
impl From<GithubReleaseOptions> for latest::GithubReleaseOptions {
fn from(value: GithubReleaseOptions) -> Self {
Self {
require_assets: value.require_assets,
pre_release: value.pre_release,
}
}
}
impl From<DownloadedFileType> for latest::DownloadedFileType {
fn from(value: DownloadedFileType) -> Self {
match value {
DownloadedFileType::Gzip => latest::DownloadedFileType::Gzip,
DownloadedFileType::GzipTar => latest::DownloadedFileType::GzipTar,
DownloadedFileType::Zip => latest::DownloadedFileType::Zip,
DownloadedFileType::Uncompressed => latest::DownloadedFileType::Uncompressed,
}
}
}
impl From<latest::LanguageServerConfig> for LanguageServerConfig {
fn from(value: latest::LanguageServerConfig) -> Self {
Self {
name: value.name,
language_name: value.language_name,
}
}
}
impl From<Command> for latest::Command {
fn from(value: Command) -> Self {
Self {
command: value.command,
args: value.args,
env: value.env,
}
}
}
#[async_trait]
impl HostWorktree for WasmState {
async fn read_text_file(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
path: String,
) -> wasmtime::Result<Result<String, String>> {
latest::HostWorktree::read_text_file(self, delegate, path).await
}
async fn shell_env(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
) -> wasmtime::Result<EnvVars> {
latest::HostWorktree::shell_env(self, delegate).await
}
async fn which(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
binary_name: String,
) -> wasmtime::Result<Option<String>> {
latest::HostWorktree::which(self, delegate, binary_name).await
}
fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
Ok(())
}
}
#[async_trait]
impl ExtensionImports for WasmState {
async fn node_binary_path(&mut self) -> wasmtime::Result<Result<String, String>> {
latest::ExtensionImports::node_binary_path(self).await
}
async fn npm_package_latest_version(
&mut self,
package_name: String,
) -> wasmtime::Result<Result<String, String>> {
latest::ExtensionImports::npm_package_latest_version(self, package_name).await
}
async fn npm_package_installed_version(
&mut self,
package_name: String,
) -> wasmtime::Result<Result<Option<String>, String>> {
latest::ExtensionImports::npm_package_installed_version(self, package_name).await
}
async fn npm_install_package(
&mut self,
package_name: String,
version: String,
) -> wasmtime::Result<Result<(), String>> {
latest::ExtensionImports::npm_install_package(self, package_name, version).await
}
async fn latest_github_release(
&mut self,
repo: String,
options: GithubReleaseOptions,
) -> wasmtime::Result<Result<GithubRelease, String>> {
Ok(
latest::ExtensionImports::latest_github_release(self, repo, options.into())
.await?
.map(|github| github.into()),
)
}
async fn current_platform(&mut self) -> Result<(Os, Architecture)> {
latest::ExtensionImports::current_platform(self)
.await
.map(|(os, arch)| (os.into(), arch.into()))
}
async fn set_language_server_installation_status(
&mut self,
server_name: String,
status: LanguageServerInstallationStatus,
) -> wasmtime::Result<()> {
let status = match status {
LanguageServerInstallationStatus::CheckingForUpdate => {
LanguageServerBinaryStatus::CheckingForUpdate
}
LanguageServerInstallationStatus::Downloading => {
LanguageServerBinaryStatus::Downloading
}
LanguageServerInstallationStatus::Cached
| LanguageServerInstallationStatus::Downloaded => LanguageServerBinaryStatus::None,
LanguageServerInstallationStatus::Failed(error) => {
LanguageServerBinaryStatus::Failed { error }
}
};
self.host
.language_registry
.update_lsp_status(language::LanguageServerName(server_name.into()), status);
Ok(())
}
async fn download_file(
&mut self,
url: String,
path: String,
file_type: DownloadedFileType,
) -> wasmtime::Result<Result<(), String>> {
latest::ExtensionImports::download_file(self, url, path, file_type.into()).await
}
}

View File

@ -0,0 +1,284 @@
use crate::wasm_host::WasmState;
use anyhow::{anyhow, Result};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use async_trait::async_trait;
use futures::io::BufReader;
use language::{LanguageServerBinaryStatus, LspAdapterDelegate};
use std::{
env,
path::PathBuf,
sync::{Arc, OnceLock},
};
use util::{maybe, SemanticVersion};
use wasmtime::component::{Linker, Resource};
pub const VERSION: SemanticVersion = SemanticVersion {
major: 0,
minor: 0,
patch: 4,
};
wasmtime::component::bindgen!({
async: true,
path: "../extension_api/wit/0.0.4",
with: {
"worktree": ExtensionWorktree,
},
});
pub type ExtensionWorktree = Arc<dyn LspAdapterDelegate>;
pub fn linker() -> &'static Linker<WasmState> {
static LINKER: OnceLock<Linker<WasmState>> = OnceLock::new();
LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker))
}
#[async_trait]
impl HostWorktree for WasmState {
async fn read_text_file(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
path: String,
) -> wasmtime::Result<Result<String, String>> {
let delegate = self.table.get(&delegate)?;
Ok(delegate
.read_text_file(path.into())
.await
.map_err(|error| error.to_string()))
}
async fn shell_env(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
) -> wasmtime::Result<EnvVars> {
let delegate = self.table.get(&delegate)?;
Ok(delegate.shell_env().await.into_iter().collect())
}
async fn which(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
binary_name: String,
) -> wasmtime::Result<Option<String>> {
let delegate = self.table.get(&delegate)?;
Ok(delegate
.which(binary_name.as_ref())
.await
.map(|path| path.to_string_lossy().to_string()))
}
fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
// we only ever hand out borrows of worktrees
Ok(())
}
}
#[async_trait]
impl ExtensionImports for WasmState {
async fn node_binary_path(&mut self) -> wasmtime::Result<Result<String, String>> {
convert_result(
self.host
.node_runtime
.binary_path()
.await
.map(|path| path.to_string_lossy().to_string()),
)
}
async fn npm_package_latest_version(
&mut self,
package_name: String,
) -> wasmtime::Result<Result<String, String>> {
convert_result(
self.host
.node_runtime
.npm_package_latest_version(&package_name)
.await,
)
}
async fn npm_package_installed_version(
&mut self,
package_name: String,
) -> wasmtime::Result<Result<Option<String>, String>> {
convert_result(
self.host
.node_runtime
.npm_package_installed_version(&self.work_dir(), &package_name)
.await,
)
}
async fn npm_install_package(
&mut self,
package_name: String,
version: String,
) -> wasmtime::Result<Result<(), String>> {
convert_result(
self.host
.node_runtime
.npm_install_packages(&self.work_dir(), &[(&package_name, &version)])
.await,
)
}
async fn latest_github_release(
&mut self,
repo: String,
options: GithubReleaseOptions,
) -> wasmtime::Result<Result<GithubRelease, String>> {
convert_result(
maybe!(async {
let release = util::github::latest_github_release(
&repo,
options.require_assets,
options.pre_release,
self.host.http_client.clone(),
)
.await?;
Ok(GithubRelease {
version: release.tag_name,
assets: release
.assets
.into_iter()
.map(|asset| GithubReleaseAsset {
name: asset.name,
download_url: asset.browser_download_url,
})
.collect(),
})
})
.await,
)
}
async fn current_platform(&mut self) -> Result<(Os, Architecture)> {
Ok((
match env::consts::OS {
"macos" => Os::Mac,
"linux" => Os::Linux,
"windows" => Os::Windows,
_ => panic!("unsupported os"),
},
match env::consts::ARCH {
"aarch64" => Architecture::Aarch64,
"x86" => Architecture::X86,
"x86_64" => Architecture::X8664,
_ => panic!("unsupported architecture"),
},
))
}
async fn set_language_server_installation_status(
&mut self,
server_name: String,
status: LanguageServerInstallationStatus,
) -> wasmtime::Result<()> {
let status = match status {
LanguageServerInstallationStatus::CheckingForUpdate => {
LanguageServerBinaryStatus::CheckingForUpdate
}
LanguageServerInstallationStatus::Downloading => {
LanguageServerBinaryStatus::Downloading
}
LanguageServerInstallationStatus::None => LanguageServerBinaryStatus::None,
LanguageServerInstallationStatus::Failed(error) => {
LanguageServerBinaryStatus::Failed { error }
}
};
self.host
.language_registry
.update_lsp_status(language::LanguageServerName(server_name.into()), status);
Ok(())
}
async fn download_file(
&mut self,
url: String,
path: String,
file_type: DownloadedFileType,
) -> wasmtime::Result<Result<(), String>> {
let result = maybe!(async {
let path = PathBuf::from(path);
let extension_work_dir = self.host.work_dir.join(self.manifest.id.as_ref());
self.host.fs.create_dir(&extension_work_dir).await?;
let destination_path = self
.host
.writeable_path_from_extension(&self.manifest.id, &path)?;
let mut response = self
.host
.http_client
.get(&url, Default::default(), true)
.await
.map_err(|err| anyhow!("error downloading release: {}", err))?;
if !response.status().is_success() {
Err(anyhow!(
"download failed with status {}",
response.status().to_string()
))?;
}
let body = BufReader::new(response.body_mut());
match file_type {
DownloadedFileType::Uncompressed => {
futures::pin_mut!(body);
self.host
.fs
.create_file_with(&destination_path, body)
.await?;
}
DownloadedFileType::Gzip => {
let body = GzipDecoder::new(body);
futures::pin_mut!(body);
self.host
.fs
.create_file_with(&destination_path, body)
.await?;
}
DownloadedFileType::GzipTar => {
let body = GzipDecoder::new(body);
futures::pin_mut!(body);
self.host
.fs
.extract_tar_file(&destination_path, Archive::new(body))
.await?;
}
DownloadedFileType::Zip => {
let file_name = destination_path
.file_name()
.ok_or_else(|| anyhow!("invalid download path"))?
.to_string_lossy();
let zip_filename = format!("{file_name}.zip");
let mut zip_path = destination_path.clone();
zip_path.set_file_name(zip_filename);
futures::pin_mut!(body);
self.host.fs.create_file_with(&zip_path, body).await?;
let unzip_status = std::process::Command::new("unzip")
.current_dir(&extension_work_dir)
.arg(&zip_path)
.output()?
.status;
if !unzip_status.success() {
Err(anyhow!("failed to unzip {} archive", path.display()))?;
}
}
}
Ok(())
})
.await;
convert_result(result)
}
}
fn convert_result<T>(result: Result<T>) -> wasmtime::Result<Result<T, String>> {
Ok(result.map_err(|error| error.to_string()))
}

View File

@ -1,6 +1,6 @@
[package]
name = "zed_extension_api"
version = "0.0.1"
version = "0.0.4"
description = "APIs for creating Zed extensions in Rust"
repository = "https://github.com/zed-industries/zed"
documentation = "https://docs.rs/zed_extension_api"
@ -15,7 +15,7 @@ workspace = true
path = "src/extension_api.rs"
[dependencies]
wit-bindgen = "0.18"
wit-bindgen = "0.22"
[package.metadata.component]
target = { path = "wit" }

View File

@ -1,6 +1,4 @@
pub struct Guest;
pub use wit::*;
pub type Result<T, E = String> = core::result::Result<T, E>;
pub trait Extension: Send + Sync {
@ -10,14 +8,14 @@ pub trait Extension: Send + Sync {
fn language_server_command(
&mut self,
config: wit::LanguageServerConfig,
worktree: &wit::Worktree,
config: LanguageServerConfig,
worktree: &Worktree,
) -> Result<Command>;
fn language_server_initialization_options(
&mut self,
_config: wit::LanguageServerConfig,
_worktree: &wit::Worktree,
_config: LanguageServerConfig,
_worktree: &Worktree,
) -> Result<Option<String>> {
Ok(None)
}
@ -54,11 +52,13 @@ pub static ZED_API_VERSION: [u8; 6] = *include_bytes!(concat!(env!("OUT_DIR"), "
mod wit {
wit_bindgen::generate!({
exports: { world: super::Component },
skip: ["init-extension"]
skip: ["init-extension"],
path: "./wit/0.0.4",
});
}
wit::export!(Component);
struct Component;
impl wit::Guest for Component {

View File

@ -48,6 +48,9 @@ world extension {
/// Gets the current operating system and architecture
import current-platform: func() -> tuple<os, architecture>;
/// Get the path to the node binary used by Zed.
import node-binary-path: func() -> result<string, string>;
/// Gets the latest version of the given NPM package.
import npm-package-latest-version: func(package-name: string) -> result<string, string>;

View File

@ -0,0 +1,93 @@
package zed:extension;
world extension {
export init-extension: func();
record github-release {
version: string,
assets: list<github-release-asset>,
}
record github-release-asset {
name: string,
download-url: string,
}
record github-release-options {
require-assets: bool,
pre-release: bool,
}
enum os {
mac,
linux,
windows,
}
enum architecture {
aarch64,
x86,
x8664,
}
enum downloaded-file-type {
gzip,
gzip-tar,
zip,
uncompressed,
}
variant language-server-installation-status {
none,
downloading,
checking-for-update,
failed(string),
}
/// Gets the current operating system and architecture
import current-platform: func() -> tuple<os, architecture>;
/// Get the path to the node binary used by Zed.
import node-binary-path: func() -> result<string, string>;
/// Gets the latest version of the given NPM package.
import npm-package-latest-version: func(package-name: string) -> result<string, string>;
/// Returns the installed version of the given NPM package, if it exists.
import npm-package-installed-version: func(package-name: string) -> result<option<string>, string>;
/// Installs the specified NPM package.
import npm-install-package: func(package-name: string, version: string) -> result<_, string>;
/// Gets the latest release for the given GitHub repository.
import latest-github-release: func(repo: string, options: github-release-options) -> result<github-release, string>;
/// Downloads a file from the given url, and saves it to the given filename within the extension's
/// working directory. Extracts the file according to the given file type.
import download-file: func(url: string, output-filename: string, file-type: downloaded-file-type) -> result<_, string>;
/// Updates the installation status for the given language server.
import set-language-server-installation-status: func(language-server-name: string, status: language-server-installation-status);
type env-vars = list<tuple<string, string>>;
record command {
command: string,
args: list<string>,
env: env-vars,
}
resource worktree {
read-text-file: func(path: string) -> result<string, string>;
which: func(binary-name: string) -> option<string>;
shell-env: func() -> env-vars;
}
record language-server-config {
name: string,
language-name: string,
}
export language-server-command: func(config: language-server-config, worktree: borrow<worktree>) -> result<command, string>;
export language-server-initialization-options: func(config: language-server-config, worktree: borrow<worktree>) -> result<option<string>, string>;
}

View File

@ -29,6 +29,7 @@ pub fn suggested_extension(file_extension_or_name: &str) -> Option<Arc<str>> {
("git-firefly", "MERGE_MSG"),
("git-firefly", "NOTES_EDITMSG"),
("git-firefly", "TAG_EDITMSG"),
("gleam", "gleam"),
("graphql", "gql"),
("graphql", "graphql"),
("java", "java"),
@ -39,6 +40,7 @@ pub fn suggested_extension(file_extension_or_name: &str) -> Option<Arc<str>> {
("r", "r"),
("r", "R"),
("sql", "sql"),
("svelte", "svelte"),
("swift", "swift"),
("templ", "templ"),
("wgsl", "wgsl"),

View File

@ -134,11 +134,15 @@ pub struct CachedLspAdapter {
pub language_ids: HashMap<String, String>,
pub adapter: Arc<dyn LspAdapter>,
pub reinstall_attempt_count: AtomicU64,
/// Indicates whether this language server is the primary language server
/// for a given language. Currently, most LSP-backed features only work
/// with one language server, so one server needs to be primary.
pub is_primary: bool,
cached_binary: futures::lock::Mutex<Option<LanguageServerBinary>>,
}
impl CachedLspAdapter {
pub fn new(adapter: Arc<dyn LspAdapter>) -> Arc<Self> {
pub fn new(adapter: Arc<dyn LspAdapter>, is_primary: bool) -> Arc<Self> {
let name = adapter.name();
let disk_based_diagnostic_sources = adapter.disk_based_diagnostic_sources();
let disk_based_diagnostics_progress_token = adapter.disk_based_diagnostics_progress_token();
@ -150,6 +154,7 @@ impl CachedLspAdapter {
disk_based_diagnostics_progress_token,
language_ids,
adapter,
is_primary,
cached_binary: Default::default(),
reinstall_attempt_count: AtomicU64::new(0),
})
@ -293,7 +298,6 @@ pub trait LspAdapter: 'static + Send + Sync {
.cached_server_binary(container_dir.to_path_buf(), delegate.as_ref())
.await
{
delegate.update_status(self.name(), LanguageServerBinaryStatus::Cached);
log::info!(
"failed to fetch newest version of language server {:?}. falling back to using {:?}",
self.name(),
@ -464,7 +468,7 @@ async fn try_fetch_server_binary<L: LspAdapter + 'static + Send + Sync + ?Sized>
.fetch_server_binary(latest_version, container_dir, delegate.as_ref())
.await;
delegate.update_status(name.clone(), LanguageServerBinaryStatus::Downloaded);
delegate.update_status(name.clone(), LanguageServerBinaryStatus::None);
binary
}

View File

@ -25,7 +25,7 @@ use sum_tree::Bias;
use text::{Point, Rope};
use theme::Theme;
use unicase::UniCase;
use util::{paths::PathExt, post_inc, ResultExt};
use util::{maybe, paths::PathExt, post_inc, ResultExt};
pub struct LanguageRegistry {
state: RwLock<LanguageRegistryState>,
@ -54,10 +54,9 @@ struct LanguageRegistryState {
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum LanguageServerBinaryStatus {
None,
CheckingForUpdate,
Downloading,
Downloaded,
Cached,
Failed { error: String },
}
@ -91,9 +90,10 @@ enum AvailableGrammar {
Loaded(#[allow(unused)] PathBuf, tree_sitter::Language),
Loading(
#[allow(unused)] PathBuf,
Vec<oneshot::Sender<Result<tree_sitter::Language>>>,
Vec<oneshot::Sender<Result<tree_sitter::Language, Arc<anyhow::Error>>>>,
),
Unloaded(PathBuf),
LoadFailed(Arc<anyhow::Error>),
}
#[derive(Debug)]
@ -213,7 +213,20 @@ impl LanguageRegistry {
.lsp_adapters
.entry(language_name)
.or_default()
.push(CachedLspAdapter::new(adapter));
.push(CachedLspAdapter::new(adapter, true));
}
pub fn register_secondary_lsp_adapter(
&self,
language_name: Arc<str>,
adapter: Arc<dyn LspAdapter>,
) {
self.state
.write()
.lsp_adapters
.entry(language_name)
.or_default()
.push(CachedLspAdapter::new(adapter, false));
}
#[cfg(any(feature = "test-support", test))]
@ -227,7 +240,7 @@ impl LanguageRegistry {
.lsp_adapters
.entry(language_name.into())
.or_default()
.push(CachedLspAdapter::new(Arc::new(adapter)));
.push(CachedLspAdapter::new(Arc::new(adapter), true));
self.fake_language_servers(language_name)
}
@ -578,6 +591,9 @@ impl LanguageRegistry {
if let Some(grammar) = state.grammars.get_mut(name.as_ref()) {
match grammar {
AvailableGrammar::LoadFailed(error) => {
tx.send(Err(error.clone())).ok();
}
AvailableGrammar::Native(grammar) | AvailableGrammar::Loaded(_, grammar) => {
tx.send(Ok(grammar.clone())).ok();
}
@ -586,46 +602,47 @@ impl LanguageRegistry {
}
AvailableGrammar::Unloaded(wasm_path) => {
let this = self.clone();
let wasm_path = wasm_path.clone();
*grammar = AvailableGrammar::Loading(wasm_path.clone(), vec![tx]);
self.executor
.spawn({
let wasm_path = wasm_path.clone();
async move {
.spawn(async move {
let grammar_result = maybe!({
let wasm_bytes = std::fs::read(&wasm_path)?;
let grammar_name = wasm_path
.file_stem()
.and_then(OsStr::to_str)
.ok_or_else(|| anyhow!("invalid grammar filename"))?;
let grammar = PARSER.with(|parser| {
anyhow::Ok(PARSER.with(|parser| {
let mut parser = parser.borrow_mut();
let mut store = parser.take_wasm_store().unwrap();
let grammar = store.load_language(&grammar_name, &wasm_bytes);
parser.set_wasm_store(store).unwrap();
grammar
})?;
})?)
})
.map_err(Arc::new);
if let Some(AvailableGrammar::Loading(_, txs)) =
this.state.write().grammars.insert(
name,
AvailableGrammar::Loaded(wasm_path, grammar.clone()),
)
{
for tx in txs {
tx.send(Ok(grammar.clone())).ok();
}
let value = match &grammar_result {
Ok(grammar) => AvailableGrammar::Loaded(wasm_path, grammar.clone()),
Err(error) => AvailableGrammar::LoadFailed(error.clone()),
};
let old_value = this.state.write().grammars.insert(name, value);
if let Some(AvailableGrammar::Loading(_, txs)) = old_value {
for tx in txs {
tx.send(grammar_result.clone()).ok();
}
anyhow::Ok(())
}
})
.detach();
*grammar = AvailableGrammar::Loading(wasm_path.clone(), vec![tx]);
}
}
} else {
tx.send(Err(anyhow!("no such grammar {}", name))).ok();
tx.send(Err(Arc::new(anyhow!("no such grammar {}", name))))
.ok();
}
async move { rx.await? }
async move { rx.await?.map_err(|e| anyhow!(e)) }
}
pub fn to_vec(&self) -> Vec<Arc<Language>> {
@ -691,7 +708,7 @@ impl LanguageRegistry {
// the login shell to be set on our process.
login_shell_env_loaded.await;
let binary = adapter
let binary_result = adapter
.clone()
.get_language_server_command(
language.clone(),
@ -699,8 +716,11 @@ impl LanguageRegistry {
delegate.clone(),
&mut cx,
)
.await?;
.await;
delegate.update_status(adapter.name.clone(), LanguageServerBinaryStatus::None);
let binary = binary_result?;
let options = adapter
.adapter
.clone()

View File

@ -49,7 +49,6 @@ tree-sitter-elixir.workspace = true
tree-sitter-elm.workspace = true
tree-sitter-embedded-template.workspace = true
tree-sitter-erlang.workspace = true
tree-sitter-gleam.workspace = true
tree-sitter-glsl.workspace = true
tree-sitter-go.workspace = true
tree-sitter-gomod.workspace = true
@ -75,7 +74,6 @@ tree-sitter-regex.workspace = true
tree-sitter-ruby.workspace = true
tree-sitter-rust.workspace = true
tree-sitter-scheme.workspace = true
tree-sitter-svelte.workspace = true
tree-sitter-toml.workspace = true
tree-sitter-typescript.workspace = true
tree-sitter-vue.workspace = true

View File

@ -12,7 +12,7 @@ use std::{
path::{Path, PathBuf},
sync::Arc,
};
use util::{async_maybe, ResultExt};
use util::{maybe, ResultExt};
const SERVER_PATH: &str = "node_modules/@astrojs/language-server/bin/nodeServer.js";
@ -107,7 +107,7 @@ async fn get_cached_server_binary(
container_dir: PathBuf,
node: &dyn NodeRuntime,
) -> Option<LanguageServerBinary> {
async_maybe!({
maybe!(async {
let mut last_version_dir = None;
let mut entries = fs::read_dir(&container_dir).await?;
while let Some(entry) = entries.next().await {

View File

@ -7,10 +7,9 @@ use lsp::LanguageServerBinary;
use smol::fs::{self, File};
use std::{any::Any, env::consts, path::PathBuf, sync::Arc};
use util::{
async_maybe,
fs::remove_matching,
github::{latest_github_release, GitHubLspBinaryVersion},
ResultExt,
maybe, ResultExt,
};
pub struct CLspAdapter;
@ -264,7 +263,7 @@ impl super::LspAdapter for CLspAdapter {
}
async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
async_maybe!({
maybe!(async {
let mut last_clangd_dir = None;
let mut entries = fs::read_dir(&container_dir).await?;
while let Some(entry) = entries.next().await {

View File

@ -9,8 +9,7 @@ use smol::fs;
use std::env::consts::ARCH;
use std::ffi::OsString;
use std::{any::Any, path::PathBuf};
use util::async_maybe;
use util::github::latest_github_release;
use util::{github::latest_github_release, maybe};
use util::{github::GitHubLspBinaryVersion, ResultExt};
pub struct OmniSharpAdapter;
@ -115,7 +114,7 @@ impl super::LspAdapter for OmniSharpAdapter {
}
async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
async_maybe!({
maybe!(async {
let mut last_binary_path = None;
let mut entries = fs::read_dir(&container_dir).await?;
while let Some(entry) = entries.next().await {

View File

@ -12,7 +12,7 @@ use std::{
path::{Path, PathBuf},
sync::Arc,
};
use util::{async_maybe, ResultExt};
use util::{maybe, ResultExt};
const SERVER_PATH: &str =
"node_modules/vscode-langservers-extracted/bin/vscode-css-language-server";
@ -105,7 +105,7 @@ async fn get_cached_server_binary(
container_dir: PathBuf,
node: &dyn NodeRuntime,
) -> Option<LanguageServerBinary> {
async_maybe!({
maybe!(async {
let mut last_version_dir = None;
let mut entries = fs::read_dir(&container_dir).await?;
while let Some(entry) = entries.next().await {

View File

@ -11,10 +11,9 @@ use settings::Settings;
use smol::{fs, fs::File};
use std::{any::Any, env::consts, ffi::OsString, path::PathBuf, sync::Arc};
use util::{
async_maybe,
fs::remove_matching,
github::{latest_github_release, GitHubLspBinaryVersion},
ResultExt,
maybe, ResultExt,
};
#[derive(Clone, Serialize, Deserialize, JsonSchema)]
@ -207,7 +206,7 @@ impl LspAdapter for DenoLspAdapter {
}
async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
async_maybe!({
maybe!(async {
let mut last = None;
let mut entries = fs::read_dir(&container_dir).await?;
while let Some(entry) = entries.next().await {

View File

@ -11,7 +11,7 @@ use std::{
path::{Path, PathBuf},
sync::Arc,
};
use util::{async_maybe, ResultExt};
use util::{maybe, ResultExt};
const SERVER_PATH: &str = "node_modules/dockerfile-language-server-nodejs/bin/docker-langserver";
@ -94,7 +94,7 @@ async fn get_cached_server_binary(
container_dir: PathBuf,
node: &dyn NodeRuntime,
) -> Option<LanguageServerBinary> {
async_maybe!({
maybe!(async {
let mut last_version_dir = None;
let mut entries = fs::read_dir(&container_dir).await?;
while let Some(entry) = entries.next().await {

View File

@ -20,10 +20,9 @@ use std::{
};
use task::static_source::{Definition, TaskDefinitions};
use util::{
async_maybe,
fs::remove_matching,
github::{latest_github_release, GitHubLspBinaryVersion},
ResultExt,
maybe, ResultExt,
};
#[derive(Clone, Serialize, Deserialize, JsonSchema)]
@ -413,7 +412,7 @@ impl LspAdapter for NextLspAdapter {
}
async fn get_cached_server_binary_next(container_dir: PathBuf) -> Option<LanguageServerBinary> {
async_maybe!({
maybe!(async {
let mut last_binary_path = None;
let mut entries = fs::read_dir(&container_dir).await?;
while let Some(entry) = entries.next().await {

View File

@ -15,7 +15,7 @@ use std::{
path::{Path, PathBuf},
sync::Arc,
};
use util::{async_maybe, ResultExt};
use util::{maybe, ResultExt};
const SERVER_NAME: &str = "elm-language-server";
const SERVER_PATH: &str = "node_modules/@elm-tooling/elm-language-server/out/node/index.js";
@ -120,7 +120,7 @@ async fn get_cached_server_binary(
container_dir: PathBuf,
node: &dyn NodeRuntime,
) -> Option<LanguageServerBinary> {
async_maybe!({
maybe!(async {
let mut last_version_dir = None;
let mut entries = fs::read_dir(&container_dir).await?;
while let Some(entry) = entries.next().await {

View File

@ -1,122 +0,0 @@
use std::any::Any;
use std::env::consts;
use std::ffi::OsString;
use std::path::PathBuf;
use anyhow::{anyhow, bail, Result};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use async_trait::async_trait;
use futures::io::BufReader;
use futures::StreamExt;
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
use lsp::LanguageServerBinary;
use smol::fs;
use util::github::{latest_github_release, GitHubLspBinaryVersion};
use util::{async_maybe, ResultExt};
fn server_binary_arguments() -> Vec<OsString> {
vec!["lsp".into()]
}
pub struct GleamLspAdapter;
#[async_trait(?Send)]
impl LspAdapter for GleamLspAdapter {
fn name(&self) -> LanguageServerName {
LanguageServerName("gleam".into())
}
async fn fetch_latest_server_version(
&self,
delegate: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
let release =
latest_github_release("gleam-lang/gleam", true, false, delegate.http_client()).await?;
let asset_name = format!(
"gleam-{version}-{arch}-{os}.tar.gz",
version = release.tag_name,
arch = std::env::consts::ARCH,
os = match consts::OS {
"macos" => "apple-darwin",
"linux" => "unknown-linux-musl",
"windows" => "pc-windows-msvc",
other => bail!("Running on unsupported os: {other}"),
},
);
let asset = release
.assets
.iter()
.find(|asset| asset.name == asset_name)
.ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
Ok(Box::new(GitHubLspBinaryVersion {
name: release.tag_name,
url: asset.browser_download_url.clone(),
}))
}
async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,
container_dir: PathBuf,
delegate: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
let binary_path = container_dir.join("gleam");
if fs::metadata(&binary_path).await.is_err() {
let mut response = delegate
.http_client()
.get(&version.url, Default::default(), true)
.await
.map_err(|err| anyhow!("error downloading release: {}", err))?;
let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
let archive = Archive::new(decompressed_bytes);
archive.unpack(container_dir).await?;
}
Ok(LanguageServerBinary {
path: binary_path,
env: None,
arguments: server_binary_arguments(),
})
}
async fn cached_server_binary(
&self,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
get_cached_server_binary(container_dir).await
}
async fn installation_test_binary(
&self,
container_dir: PathBuf,
) -> Option<LanguageServerBinary> {
get_cached_server_binary(container_dir)
.await
.map(|mut binary| {
binary.arguments = vec!["--version".into()];
binary
})
}
}
async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
async_maybe!({
let mut last = None;
let mut entries = fs::read_dir(&container_dir).await?;
while let Some(entry) = entries.next().await {
last = Some(entry?.path());
}
anyhow::Ok(LanguageServerBinary {
path: last.ok_or_else(|| anyhow!("no cached binary"))?,
env: None,
arguments: server_binary_arguments(),
})
})
.await
.log_err()
}

View File

@ -1,11 +0,0 @@
name = "Gleam"
grammar = "gleam"
path_suffixes = ["gleam"]
line_comments = ["// ", "/// "]
autoclose_before = ";:.,=}])>"
brackets = [
{ start = "{", end = "}", close = true, newline = true },
{ start = "[", end = "]", close = true, newline = true },
{ start = "(", end = ")", close = true, newline = true },
{ start = "\"", end = "\"", close = true, newline = false, not_in = ["string", "comment"] },
]

View File

@ -1,130 +0,0 @@
; Comments
(module_comment) @comment
(statement_comment) @comment
(comment) @comment
; Constants
(constant
name: (identifier) @constant)
; Modules
(module) @module
(import alias: (identifier) @module)
(remote_type_identifier
module: (identifier) @module)
(remote_constructor_name
module: (identifier) @module)
((field_access
record: (identifier) @module
field: (label) @function)
(#is-not? local))
; Functions
(unqualified_import (identifier) @function)
(unqualified_import "type" (type_identifier) @type)
(unqualified_import (type_identifier) @constructor)
(function
name: (identifier) @function)
(external_function
name: (identifier) @function)
(function_parameter
name: (identifier) @variable.parameter)
((function_call
function: (identifier) @function)
(#is-not? local))
((binary_expression
operator: "|>"
right: (identifier) @function)
(#is-not? local))
; "Properties"
; Assumed to be intended to refer to a name for a field; something that comes
; before ":" or after "."
; e.g. record field names, tuple indices, names for named arguments, etc
(label) @property
(tuple_access
index: (integer) @property)
; Attributes
(attribute
"@" @attribute
name: (identifier) @attribute)
(attribute_value (identifier) @constant)
; Type names
(remote_type_identifier) @type
(type_identifier) @type
; Data constructors
(constructor_name) @constructor
; Literals
(string) @string
((escape_sequence) @warning
; Deprecated in v0.33.0-rc2:
(#eq? @warning "\\e"))
(escape_sequence) @string.escape
(bit_string_segment_option) @function.builtin
(integer) @number
(float) @number
; Reserved identifiers
; TODO: when tree-sitter supports `#any-of?` in the Rust bindings,
; refactor this to use `#any-of?` rather than `#match?`
((identifier) @warning
(#match? @warning "^(auto|delegate|derive|else|implement|macro|test|echo)$"))
; Variables
(identifier) @variable
(discard) @comment.unused
; Keywords
[
(visibility_modifier) ; "pub"
(opacity_modifier) ; "opaque"
"as"
"assert"
"case"
"const"
; DEPRECATED: 'external' was removed in v0.30.
"external"
"fn"
"if"
"import"
"let"
"panic"
"todo"
"type"
"use"
] @keyword
; Operators
(binary_expression
operator: _ @operator)
(boolean_negation "!" @operator)
(integer_negation "-" @operator)
; Punctuation
[
"("
")"
"["
"]"
"{"
"}"
"<<"
">>"
] @punctuation.bracket
[
"."
","
;; Controversial -- maybe some are operators?
":"
"#"
"="
"->"
".."
"-"
"<-"
] @punctuation.delimiter

View File

@ -1,3 +0,0 @@
(_ "[" "]" @end) @indent
(_ "{" "}" @end) @indent
(_ "(" ")" @end) @indent

View File

@ -1,31 +0,0 @@
(external_type
(visibility_modifier)? @context
"type" @context
(type_name) @name) @item
(type_definition
(visibility_modifier)? @context
(opacity_modifier)? @context
"type" @context
(type_name) @name) @item
(data_constructor
(constructor_name) @name) @item
(data_constructor_argument
(label) @name) @item
(type_alias
(visibility_modifier)? @context
"type" @context
(type_name) @name) @item
(function
(visibility_modifier)? @context
"fn" @context
name: (_) @name) @item
(constant
(visibility_modifier)? @context
"const" @context
name: (_) @name) @item

View File

@ -19,7 +19,7 @@ use std::{
Arc,
},
};
use util::{async_maybe, fs::remove_matching, github::latest_github_release, ResultExt};
use util::{fs::remove_matching, github::latest_github_release, maybe, ResultExt};
fn server_binary_arguments() -> Vec<OsString> {
vec!["-mode=stdio".into()]
@ -368,7 +368,7 @@ impl super::LspAdapter for GoLspAdapter {
}
async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
async_maybe!({
maybe!(async {
let mut last_binary_path = None;
let mut entries = fs::read_dir(&container_dir).await?;
while let Some(entry) = entries.next().await {

View File

@ -12,7 +12,7 @@ use std::{
path::{Path, PathBuf},
sync::Arc,
};
use util::{async_maybe, ResultExt};
use util::{maybe, ResultExt};
const SERVER_PATH: &str =
"node_modules/vscode-langservers-extracted/bin/vscode-html-language-server";
@ -105,7 +105,7 @@ async fn get_cached_server_binary(
container_dir: PathBuf,
node: &dyn NodeRuntime,
) -> Option<LanguageServerBinary> {
async_maybe!({
maybe!(async {
let mut last_version_dir = None;
let mut entries = fs::read_dir(&container_dir).await?;
while let Some(entry) = entries.next().await {

View File

@ -16,7 +16,7 @@ use std::{
path::{Path, PathBuf},
sync::{Arc, OnceLock},
};
use util::{async_maybe, paths, ResultExt};
use util::{maybe, paths, ResultExt};
const SERVER_PATH: &str = "node_modules/vscode-json-languageserver/bin/vscode-json-languageserver";
@ -167,7 +167,7 @@ async fn get_cached_server_binary(
container_dir: PathBuf,
node: &dyn NodeRuntime,
) -> Option<LanguageServerBinary> {
async_maybe!({
maybe!(async {
let mut last_version_dir = None;
let mut entries = fs::read_dir(&container_dir).await?;
while let Some(entry) = entries.next().await {

View File

@ -22,7 +22,6 @@ mod dockerfile;
mod elixir;
mod elm;
mod erlang;
mod gleam;
mod go;
mod haskell;
mod html;
@ -36,7 +35,6 @@ mod purescript;
mod python;
mod ruby;
mod rust;
mod svelte;
mod tailwind;
mod terraform;
mod toml;
@ -83,7 +81,6 @@ pub fn init(
tree_sitter_embedded_template::language(),
),
("erlang", tree_sitter_erlang::language()),
("gleam", tree_sitter_gleam::language()),
("glsl", tree_sitter_glsl::language()),
("go", tree_sitter_go::language()),
("gomod", tree_sitter_gomod::language()),
@ -113,7 +110,6 @@ pub fn init(
("ruby", tree_sitter_ruby::language()),
("rust", tree_sitter_rust::language()),
("scheme", tree_sitter_scheme::language()),
("svelte", tree_sitter_svelte::language()),
("toml", tree_sitter_toml::language()),
("tsx", tree_sitter_typescript::language_tsx()),
("typescript", tree_sitter_typescript::language_typescript()),
@ -237,8 +233,6 @@ pub fn init(
}
}
language!("erlang", vec![Arc::new(erlang::ErlangLspAdapter)]);
language!("gleam", vec![Arc::new(gleam::GleamLspAdapter)]);
language!("go", vec![Arc::new(go::GoLspAdapter)]);
language!("gomod");
language!("gowork");
@ -346,13 +340,6 @@ pub fn init(
"yaml",
vec![Arc::new(yaml::YamlLspAdapter::new(node_runtime.clone()))]
);
language!(
"svelte",
vec![
Arc::new(svelte::SvelteLspAdapter::new(node_runtime.clone())),
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
]
);
language!(
"php",
vec![
@ -393,6 +380,11 @@ pub fn init(
))]
);
language!("dart", vec![Arc::new(dart::DartLanguageServer {})]);
languages.register_secondary_lsp_adapter(
"Svelte".into(),
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
);
}
#[cfg(any(test, feature = "test-support"))]

View File

@ -8,9 +8,8 @@ use lsp::LanguageServerBinary;
use smol::fs;
use std::{any::Any, env::consts, path::PathBuf};
use util::{
async_maybe,
github::{latest_github_release, GitHubLspBinaryVersion},
ResultExt,
maybe, ResultExt,
};
#[derive(Copy, Clone)]
@ -117,7 +116,7 @@ impl super::LspAdapter for LuaLspAdapter {
}
async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
async_maybe!({
maybe!(async {
let mut last_binary_path = None;
let mut entries = fs::read_dir(&container_dir).await?;
while let Some(entry) = entries.next().await {

View File

@ -14,7 +14,7 @@ use std::{
path::{Path, PathBuf},
sync::Arc,
};
use util::{async_maybe, ResultExt};
use util::{maybe, ResultExt};
fn intelephense_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
vec![server_path.into(), "--stdio".into()]
@ -106,7 +106,7 @@ async fn get_cached_server_binary(
container_dir: PathBuf,
node: &dyn NodeRuntime,
) -> Option<LanguageServerBinary> {
async_maybe!({
maybe!(async {
let mut last_version_dir = None;
let mut entries = fs::read_dir(&container_dir).await?;
while let Some(entry) = entries.next().await {

View File

@ -11,7 +11,7 @@ use std::{
path::{Path, PathBuf},
sync::Arc,
};
use util::{async_maybe, ResultExt};
use util::{maybe, ResultExt};
const SERVER_PATH: &str = "node_modules/.bin/prisma-language-server";
@ -94,7 +94,7 @@ async fn get_cached_server_binary(
container_dir: PathBuf,
node: &dyn NodeRuntime,
) -> Option<LanguageServerBinary> {
async_maybe!({
maybe!(async {
let mut last_version_dir = None;
let mut entries = fs::read_dir(&container_dir).await?;
while let Some(entry) = entries.next().await {

View File

@ -13,7 +13,7 @@ use std::{
path::{Path, PathBuf},
sync::Arc,
};
use util::{async_maybe, ResultExt};
use util::{maybe, ResultExt};
const SERVER_PATH: &str = "node_modules/.bin/purescript-language-server";
@ -115,7 +115,7 @@ async fn get_cached_server_binary(
container_dir: PathBuf,
node: &dyn NodeRuntime,
) -> Option<LanguageServerBinary> {
async_maybe!({
maybe!(async {
let mut last_version_dir = None;
let mut entries = fs::read_dir(&container_dir).await?;
while let Some(entry) = entries.next().await {

View File

@ -16,10 +16,9 @@ use task::{
TaskVariables,
};
use util::{
async_maybe,
fs::remove_matching,
github::{latest_github_release, GitHubLspBinaryVersion},
ResultExt,
maybe, ResultExt,
};
pub struct RustLspAdapter;
@ -397,7 +396,7 @@ impl ContextProvider for RustContextProvider {
}
async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
async_maybe!({
maybe!(async {
let mut last = None;
let mut entries = fs::read_dir(&container_dir).await?;
while let Some(entry) = entries.next().await {

View File

@ -1,162 +0,0 @@
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use futures::StreamExt;
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
use lsp::LanguageServerBinary;
use node_runtime::NodeRuntime;
use serde_json::json;
use smol::fs;
use std::{
any::Any,
ffi::OsString,
path::{Path, PathBuf},
sync::Arc,
};
use util::{async_maybe, ResultExt};
const SERVER_PATH: &str = "node_modules/svelte-language-server/bin/server.js";
fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
vec![server_path.into(), "--stdio".into()]
}
pub struct SvelteLspAdapter {
node: Arc<dyn NodeRuntime>,
}
impl SvelteLspAdapter {
pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
SvelteLspAdapter { node }
}
}
#[async_trait(?Send)]
impl LspAdapter for SvelteLspAdapter {
fn name(&self) -> LanguageServerName {
LanguageServerName("svelte-language-server".into())
}
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Any + Send>> {
Ok(Box::new(
self.node
.npm_package_latest_version("svelte-language-server")
.await?,
) as Box<_>)
}
async fn fetch_server_binary(
&self,
latest_version: Box<dyn 'static + Send + Any>,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let latest_version = latest_version.downcast::<String>().unwrap();
let server_path = container_dir.join(SERVER_PATH);
let package_name = "svelte-language-server";
let should_install_language_server = self
.node
.should_install_npm_package(package_name, &server_path, &container_dir, &latest_version)
.await;
if should_install_language_server {
self.node
.npm_install_packages(&container_dir, &[(package_name, latest_version.as_str())])
.await?;
}
Ok(LanguageServerBinary {
path: self.node.binary_path().await?,
env: None,
arguments: server_binary_arguments(&server_path),
})
}
async fn cached_server_binary(
&self,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
get_cached_server_binary(container_dir, &*self.node).await
}
async fn installation_test_binary(
&self,
container_dir: PathBuf,
) -> Option<LanguageServerBinary> {
get_cached_server_binary(container_dir, &*self.node).await
}
async fn initialization_options(
self: Arc<Self>,
_: &Arc<dyn LspAdapterDelegate>,
) -> Result<Option<serde_json::Value>> {
let config = json!({
"inlayHints": {
"parameterNames": {
"enabled": "all",
"suppressWhenArgumentMatchesName": false
},
"parameterTypes": {
"enabled": true
},
"variableTypes": {
"enabled": true,
"suppressWhenTypeMatchesName": false
},
"propertyDeclarationTypes": {
"enabled": true
},
"functionLikeReturnType": {
"enabled": true
},
"enumMemberValues": {
"enabled": true
}
}
});
Ok(Some(json!({
"provideFormatter": true,
"configuration": {
"typescript": config,
"javascript": config
}
})))
}
}
async fn get_cached_server_binary(
container_dir: PathBuf,
node: &dyn NodeRuntime,
) -> Option<LanguageServerBinary> {
async_maybe!({
let mut last_version_dir = None;
let mut entries = fs::read_dir(&container_dir).await?;
while let Some(entry) = entries.next().await {
let entry = entry?;
if entry.file_type().await?.is_dir() {
last_version_dir = Some(entry.path());
}
}
let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
let server_path = last_version_dir.join(SERVER_PATH);
if server_path.exists() {
Ok(LanguageServerBinary {
path: node.binary_path().await?,
env: None,
arguments: server_binary_arguments(&server_path),
})
} else {
Err(anyhow!(
"missing executable in directory {:?}",
last_version_dir
))
}
})
.await
.log_err()
}

View File

@ -1,9 +0,0 @@
[
(style_element)
(script_element)
(element)
(if_statement)
(else_statement)
(each_statement)
(await_statement)
] @fold

View File

@ -14,7 +14,7 @@ use std::{
path::{Path, PathBuf},
sync::Arc,
};
use util::{async_maybe, ResultExt};
use util::{maybe, ResultExt};
const SERVER_PATH: &str = "node_modules/.bin/tailwindcss-language-server";
@ -135,7 +135,7 @@ async fn get_cached_server_binary(
container_dir: PathBuf,
node: &dyn NodeRuntime,
) -> Option<LanguageServerBinary> {
async_maybe!({
maybe!(async {
let mut last_version_dir = None;
let mut entries = fs::read_dir(&container_dir).await?;
while let Some(entry) = entries.next().await {

View File

@ -7,10 +7,9 @@ use lsp::{CodeActionKind, LanguageServerBinary};
use smol::fs::{self, File};
use std::{any::Any, ffi::OsString, path::PathBuf};
use util::{
async_maybe,
fs::remove_matching,
github::{latest_github_release, GitHubLspBinaryVersion},
ResultExt,
maybe, ResultExt,
};
fn terraform_ls_binary_arguments() -> Vec<OsString> {
@ -154,7 +153,7 @@ fn build_download_url(version: String) -> Result<String> {
}
async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
async_maybe!({
maybe!(async {
let mut last = None;
let mut entries = fs::read_dir(&container_dir).await?;
while let Some(entry) = entries.next().await {

View File

@ -6,8 +6,8 @@ use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
use lsp::LanguageServerBinary;
use smol::fs::{self, File};
use std::{any::Any, path::PathBuf};
use util::async_maybe;
use util::github::latest_github_release;
use util::maybe;
use util::{github::GitHubLspBinaryVersion, ResultExt};
pub struct TaploLspAdapter;
@ -108,7 +108,7 @@ impl LspAdapter for TaploLspAdapter {
}
async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
async_maybe!({
maybe!(async {
let mut last = None;
let mut entries = fs::read_dir(&container_dir).await?;
while let Some(entry) = entries.next().await {

View File

@ -18,10 +18,9 @@ use std::{
sync::Arc,
};
use util::{
async_maybe,
fs::remove_matching,
github::{build_tarball_url, GitHubLspBinaryVersion},
ResultExt,
maybe, ResultExt,
};
fn typescript_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
@ -199,7 +198,7 @@ async fn get_cached_ts_server_binary(
container_dir: PathBuf,
node: &dyn NodeRuntime,
) -> Option<LanguageServerBinary> {
async_maybe!({
maybe!(async {
let old_server_path = container_dir.join(TypeScriptLspAdapter::OLD_SERVER_PATH);
let new_server_path = container_dir.join(TypeScriptLspAdapter::NEW_SERVER_PATH);
if new_server_path.exists() {
@ -378,7 +377,7 @@ async fn get_cached_eslint_server_binary(
container_dir: PathBuf,
node: &dyn NodeRuntime,
) -> Option<LanguageServerBinary> {
async_maybe!({
maybe!(async {
// This is unfortunate but we don't know what the version is to build a path directly
let mut dir = fs::read_dir(&container_dir).await?;
let first = dir.next().await.ok_or(anyhow!("missing first file"))??;

View File

@ -12,7 +12,7 @@ use std::{
path::{Path, PathBuf},
sync::Arc,
};
use util::{async_maybe, ResultExt};
use util::{maybe, ResultExt};
pub struct VueLspVersion {
vue_version: String,
@ -211,7 +211,7 @@ async fn get_cached_server_binary(
container_dir: PathBuf,
node: Arc<dyn NodeRuntime>,
) -> Option<(LanguageServerBinary, TypescriptPath)> {
async_maybe!({
maybe!(async {
let mut last_version_dir = None;
let mut entries = fs::read_dir(&container_dir).await?;
while let Some(entry) = entries.next().await {

View File

@ -15,7 +15,7 @@ use std::{
path::{Path, PathBuf},
sync::Arc,
};
use util::{async_maybe, ResultExt};
use util::{maybe, ResultExt};
const SERVER_PATH: &str = "node_modules/yaml-language-server/bin/yaml-language-server";
@ -110,7 +110,7 @@ async fn get_cached_server_binary(
container_dir: PathBuf,
node: &dyn NodeRuntime,
) -> Option<LanguageServerBinary> {
async_maybe!({
maybe!(async {
let mut last_version_dir = None;
let mut entries = fs::read_dir(&container_dir).await?;
while let Some(entry) = entries.next().await {

View File

@ -9,8 +9,8 @@ use lsp::LanguageServerBinary;
use smol::fs;
use std::env::consts::{ARCH, OS};
use std::{any::Any, path::PathBuf};
use util::async_maybe;
use util::github::latest_github_release;
use util::maybe;
use util::{github::GitHubLspBinaryVersion, ResultExt};
pub struct ZlsAdapter;
@ -113,7 +113,7 @@ impl LspAdapter for ZlsAdapter {
}
async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
async_maybe!({
maybe!(async {
let mut last_binary_path = None;
let mut entries = fs::read_dir(&container_dir).await?;
while let Some(entry) = entries.next().await {

View File

@ -9149,7 +9149,8 @@ impl Project {
buffer: &Buffer,
cx: &AppContext,
) -> Option<(&Arc<CachedLspAdapter>, &Arc<LanguageServer>)> {
self.language_servers_for_buffer(buffer, cx).next()
self.language_servers_for_buffer(buffer, cx)
.find(|s| s.0.is_primary)
}
pub fn language_server_for_buffer(

View File

@ -426,21 +426,20 @@ pub fn unzip_option<T, U>(option: Option<(T, U)>) -> (Option<T>, Option<U>) {
}
}
/// Evaluates to an immediately invoked function expression. Good for using the ? operator
/// in functions which do not return an Option or Result
/// Expands to an immediately-invoked function expression. Good for using the ? operator
/// in functions which do not return an Option or Result.
///
/// Accepts a normal block, an async block, or an async move block.
#[macro_export]
macro_rules! maybe {
($block:block) => {
(|| $block)()
};
}
/// Evaluates to an immediately invoked function expression. Good for using the ? operator
/// in functions which do not return an Option or Result, but async.
#[macro_export]
macro_rules! async_maybe {
($block:block) => {
(|| async move { $block })()
(async $block:block) => {
(|| async $block)()
};
(async move $block:block) => {
(|| async move $block)()
};
}

View File

@ -184,7 +184,7 @@ impl LanguageServerPrompt {
}
async fn select_option(this: View<Self>, ix: usize, mut cx: AsyncWindowContext) {
util::async_maybe!({
util::maybe!(async move {
let potential_future = this.update(&mut cx, |this, _| {
this.request.take().map(|request| request.respond(ix))
});

View File

@ -47,8 +47,8 @@ use std::{
};
use theme::{ActiveTheme, SystemAppearance, ThemeRegistry, ThemeSettings};
use util::{
async_maybe,
http::{HttpClient, HttpClientWithUrl},
maybe,
paths::{self, CRASHES_DIR, CRASHES_RETIRED_DIR},
ResultExt, TryFutureExt,
};
@ -455,7 +455,7 @@ async fn installation_id() -> Result<(String, bool)> {
}
async fn restore_or_create_workspace(app_state: Arc<AppState>, cx: AsyncAppContext) {
async_maybe!({
maybe!(async {
if let Some(location) = workspace::last_opened_workspace_paths().await {
cx.update(|cx| {
workspace::open_paths(

View File

@ -8,9 +8,9 @@ license = "Apache-2.0"
[lints]
workspace = true
[dependencies]
zed_extension_api = { path = "../../crates/extension_api" }
[lib]
path = "src/gleam.rs"
crate-type = ["cdylib"]
[dependencies]
zed_extension_api = "0.0.4"

View File

@ -9,10 +9,6 @@ impl GleamExtension {
fn language_server_binary_path(&mut self, config: zed::LanguageServerConfig) -> Result<String> {
if let Some(path) = &self.cached_binary_path {
if fs::metadata(path).map_or(false, |stat| stat.is_file()) {
zed::set_language_server_installation_status(
&config.name,
&zed::LanguageServerInstallationStatus::Cached,
);
return Ok(path.clone());
}
}
@ -75,11 +71,6 @@ impl GleamExtension {
fs::remove_dir_all(&entry.path()).ok();
}
}
zed::set_language_server_installation_status(
&config.name,
&zed::LanguageServerInstallationStatus::Downloaded,
);
}
self.cached_binary_path = Some(binary_path.clone());

3
extensions/svelte/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
target
*.wasm
grammars

View File

@ -0,0 +1,16 @@
[package]
name = "zed_svelte"
version = "0.0.1"
edition = "2021"
publish = false
license = "Apache-2.0"
[lints]
workspace = true
[lib]
path = "src/svelte.rs"
crate-type = ["cdylib"]
[dependencies]
zed_extension_api = "0.0.4"

View File

@ -0,0 +1,15 @@
id = "svelte"
name = "Svelte"
description = "Svelte support"
version = "0.0.1"
schema_version = 1
authors = []
repository = "https://github.com/zed-extensions/svelte"
[language_servers.svelte-language-server]
name = "Svelte Language Server"
language = "Svelte"
[grammars.svelte]
repository = "https://github.com/Himujjal/tree-sitter-svelte"
commit = "ea528fc9985aed8d93c9f438c185644a33d011af"

View File

@ -0,0 +1,126 @@
use std::{env, fs};
use zed_extension_api::{self as zed, Result};
struct SvelteExtension {
did_find_server: bool,
}
const SERVER_PATH: &str = "node_modules/svelte-language-server/bin/server.js";
const PACKAGE_NAME: &str = "svelte-language-server";
impl SvelteExtension {
fn server_exists(&self) -> bool {
fs::metadata(SERVER_PATH).map_or(false, |stat| stat.is_file())
}
fn server_script_path(&mut self, config: zed::LanguageServerConfig) -> Result<String> {
let server_exists = self.server_exists();
if self.did_find_server && server_exists {
return Ok(SERVER_PATH.to_string());
}
zed::set_language_server_installation_status(
&config.name,
&zed::LanguageServerInstallationStatus::CheckingForUpdate,
);
let version = zed::npm_package_latest_version(PACKAGE_NAME)?;
if !server_exists
|| zed::npm_package_installed_version(PACKAGE_NAME)?.as_ref() != Some(&version)
{
zed::set_language_server_installation_status(
&config.name,
&zed::LanguageServerInstallationStatus::Downloading,
);
let result = zed::npm_install_package(PACKAGE_NAME, &version);
match result {
Ok(()) => {
if !self.server_exists() {
Err(format!(
"installed package '{PACKAGE_NAME}' did not contain expected path '{SERVER_PATH}'",
))?;
}
}
Err(error) => {
if !self.server_exists() {
Err(error)?;
}
}
}
}
self.did_find_server = true;
Ok(SERVER_PATH.to_string())
}
}
impl zed::Extension for SvelteExtension {
fn new() -> Self {
Self {
did_find_server: false,
}
}
fn language_server_command(
&mut self,
config: zed::LanguageServerConfig,
_: &zed::Worktree,
) -> Result<zed::Command> {
let server_path = self.server_script_path(config)?;
Ok(zed::Command {
command: zed::node_binary_path()?,
args: vec![
env::current_dir()
.unwrap()
.join(&server_path)
.to_string_lossy()
.to_string(),
"--stdio".to_string(),
],
env: Default::default(),
})
}
fn language_server_initialization_options(
&mut self,
_: zed::LanguageServerConfig,
_: &zed::Worktree,
) -> Result<Option<String>> {
let config = r#"{
"inlayHints": {
"parameterNames": {
"enabled": "all",
"suppressWhenArgumentMatchesName": false
},
"parameterTypes": {
"enabled": true
},
"variableTypes": {
"enabled": true,
"suppressWhenTypeMatchesName": false
},
"propertyDeclarationTypes": {
"enabled": true
},
"functionLikeReturnType": {
"enabled": true
},
"enumMemberValues": {
"enabled": true
}
}
}"#;
Ok(Some(format!(
r#"{{
"provideFormatter": true,
"configuration": {{
"typescript": {config},
"javascript": {config}
}}
}}"#
)))
}
}
zed::register_extension!(SvelteExtension);

View File

@ -8,9 +8,9 @@ license = "Apache-2.0"
[lints]
workspace = true
[dependencies]
zed_extension_api = { path = "../../crates/extension_api" }
[lib]
path = "src/uiua.rs"
crate-type = ["cdylib"]
[dependencies]
zed_extension_api = "0.0.4"