Add initial support for defining language server adapters in WebAssembly-based extensions (#8645)
This PR adds **internal** ability to run arbitrary language servers via WebAssembly extensions. The functionality isn't exposed yet - we're just landing this in this early state because there have been a lot of changes to the `LspAdapter` trait, and other language server logic. ## Next steps * Currently, wasm extensions can only define how to *install* and run a language server, they can't yet implement the other LSP adapter methods, such as formatting completion labels and workspace symbols. * We don't have an automatic way to install or develop these types of extensions * We don't have a way to package these types of extensions in our extensions repo, to make them available via our extensions API. * The Rust extension API crate, `zed-extension-api` has not yet been published to crates.io, because we still consider the API a work in progress. Release Notes: - N/A --------- Co-authored-by: Marshall <marshall@zed.dev> Co-authored-by: Nathan <nathan@zed.dev> Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
This commit is contained in:
parent
f3f2225a8e
commit
268fa1cbaf
84 changed files with 3714 additions and 1973 deletions
|
@ -14,7 +14,8 @@ use notify::{Config, EventKind, Watcher};
|
|||
#[cfg(unix)]
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
||||
use futures::{future::BoxFuture, Stream, StreamExt};
|
||||
use async_tar::Archive;
|
||||
use futures::{future::BoxFuture, AsyncRead, Stream, StreamExt};
|
||||
use git2::Repository as LibGitRepository;
|
||||
use parking_lot::Mutex;
|
||||
use repository::GitRepository;
|
||||
|
@ -43,6 +44,16 @@ use std::ffi::OsStr;
|
|||
pub trait Fs: Send + Sync {
|
||||
async fn create_dir(&self, path: &Path) -> Result<()>;
|
||||
async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()>;
|
||||
async fn create_file_with(
|
||||
&self,
|
||||
path: &Path,
|
||||
content: Pin<&mut (dyn AsyncRead + Send)>,
|
||||
) -> Result<()>;
|
||||
async fn extract_tar_file(
|
||||
&self,
|
||||
path: &Path,
|
||||
content: Archive<Pin<&mut (dyn AsyncRead + Send)>>,
|
||||
) -> Result<()>;
|
||||
async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()>;
|
||||
async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()>;
|
||||
async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()>;
|
||||
|
@ -125,6 +136,25 @@ impl Fs for RealFs {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_file_with(
|
||||
&self,
|
||||
path: &Path,
|
||||
content: Pin<&mut (dyn AsyncRead + Send)>,
|
||||
) -> Result<()> {
|
||||
let mut file = smol::fs::File::create(&path).await?;
|
||||
futures::io::copy(content, &mut file).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn extract_tar_file(
|
||||
&self,
|
||||
path: &Path,
|
||||
content: Archive<Pin<&mut (dyn AsyncRead + Send)>>,
|
||||
) -> Result<()> {
|
||||
content.unpack(path).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> {
|
||||
if !options.overwrite && smol::fs::metadata(target).await.is_ok() {
|
||||
if options.ignore_if_exists {
|
||||
|
@ -429,7 +459,7 @@ enum FakeFsEntry {
|
|||
File {
|
||||
inode: u64,
|
||||
mtime: SystemTime,
|
||||
content: String,
|
||||
content: Vec<u8>,
|
||||
},
|
||||
Dir {
|
||||
inode: u64,
|
||||
|
@ -575,7 +605,7 @@ impl FakeFs {
|
|||
})
|
||||
}
|
||||
|
||||
pub async fn insert_file(&self, path: impl AsRef<Path>, content: String) {
|
||||
pub async fn insert_file(&self, path: impl AsRef<Path>, content: Vec<u8>) {
|
||||
self.write_file_internal(path, content).unwrap()
|
||||
}
|
||||
|
||||
|
@ -598,7 +628,7 @@ impl FakeFs {
|
|||
state.emit_event(&[path]);
|
||||
}
|
||||
|
||||
pub fn write_file_internal(&self, path: impl AsRef<Path>, content: String) -> Result<()> {
|
||||
fn write_file_internal(&self, path: impl AsRef<Path>, content: Vec<u8>) -> Result<()> {
|
||||
let mut state = self.state.lock();
|
||||
let path = path.as_ref();
|
||||
let inode = state.next_inode;
|
||||
|
@ -625,6 +655,16 @@ impl FakeFs {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn load_internal(&self, path: impl AsRef<Path>) -> Result<Vec<u8>> {
|
||||
let path = path.as_ref();
|
||||
let path = normalize_path(path);
|
||||
self.simulate_random_delay().await;
|
||||
let state = self.state.lock();
|
||||
let entry = state.read_path(&path)?;
|
||||
let entry = entry.lock();
|
||||
entry.file_content(&path).cloned()
|
||||
}
|
||||
|
||||
pub fn pause_events(&self) {
|
||||
self.state.lock().events_paused = true;
|
||||
}
|
||||
|
@ -662,7 +702,7 @@ impl FakeFs {
|
|||
self.create_dir(path).await.unwrap();
|
||||
}
|
||||
String(contents) => {
|
||||
self.insert_file(&path, contents).await;
|
||||
self.insert_file(&path, contents.into_bytes()).await;
|
||||
}
|
||||
_ => {
|
||||
panic!("JSON object must contain only objects, strings, or null");
|
||||
|
@ -672,6 +712,30 @@ impl FakeFs {
|
|||
.boxed()
|
||||
}
|
||||
|
||||
pub fn insert_tree_from_real_fs<'a>(
|
||||
&'a self,
|
||||
path: impl 'a + AsRef<Path> + Send,
|
||||
src_path: impl 'a + AsRef<Path> + Send,
|
||||
) -> futures::future::BoxFuture<'a, ()> {
|
||||
use futures::FutureExt as _;
|
||||
|
||||
async move {
|
||||
let path = path.as_ref();
|
||||
if std::fs::metadata(&src_path).unwrap().is_file() {
|
||||
let contents = std::fs::read(src_path).unwrap();
|
||||
self.insert_file(path, contents).await;
|
||||
} else {
|
||||
self.create_dir(path).await.unwrap();
|
||||
for entry in std::fs::read_dir(&src_path).unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
self.insert_tree_from_real_fs(&path.join(entry.file_name()), &entry.path())
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
pub fn with_git_state<F>(&self, dot_git: &Path, emit_git_event: bool, f: F)
|
||||
where
|
||||
F: FnOnce(&mut FakeGitRepositoryState),
|
||||
|
@ -832,7 +896,7 @@ impl FakeFsEntry {
|
|||
matches!(self, Self::Symlink { .. })
|
||||
}
|
||||
|
||||
fn file_content(&self, path: &Path) -> Result<&String> {
|
||||
fn file_content(&self, path: &Path) -> Result<&Vec<u8>> {
|
||||
if let Self::File { content, .. } = self {
|
||||
Ok(content)
|
||||
} else {
|
||||
|
@ -840,7 +904,7 @@ impl FakeFsEntry {
|
|||
}
|
||||
}
|
||||
|
||||
fn set_file_content(&mut self, path: &Path, new_content: String) -> Result<()> {
|
||||
fn set_file_content(&mut self, path: &Path, new_content: Vec<u8>) -> Result<()> {
|
||||
if let Self::File { content, mtime, .. } = self {
|
||||
*mtime = SystemTime::now();
|
||||
*content = new_content;
|
||||
|
@ -909,7 +973,7 @@ impl Fs for FakeFs {
|
|||
let file = Arc::new(Mutex::new(FakeFsEntry::File {
|
||||
inode,
|
||||
mtime,
|
||||
content: String::new(),
|
||||
content: Vec::new(),
|
||||
}));
|
||||
state.write_path(path, |entry| {
|
||||
match entry {
|
||||
|
@ -930,6 +994,36 @@ impl Fs for FakeFs {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_file_with(
|
||||
&self,
|
||||
path: &Path,
|
||||
mut content: Pin<&mut (dyn AsyncRead + Send)>,
|
||||
) -> Result<()> {
|
||||
let mut bytes = Vec::new();
|
||||
content.read_to_end(&mut bytes).await?;
|
||||
self.write_file_internal(path, bytes)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn extract_tar_file(
|
||||
&self,
|
||||
path: &Path,
|
||||
content: Archive<Pin<&mut (dyn AsyncRead + Send)>>,
|
||||
) -> Result<()> {
|
||||
let mut entries = content.entries()?;
|
||||
while let Some(entry) = entries.next().await {
|
||||
let mut entry = entry?;
|
||||
if entry.header().entry_type().is_file() {
|
||||
let path = path.join(entry.path()?.as_ref());
|
||||
let mut bytes = Vec::new();
|
||||
entry.read_to_end(&mut bytes).await?;
|
||||
self.create_dir(path.parent().unwrap()).await?;
|
||||
self.write_file_internal(&path, bytes)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn rename(&self, old_path: &Path, new_path: &Path, options: RenameOptions) -> Result<()> {
|
||||
self.simulate_random_delay().await;
|
||||
|
||||
|
@ -1000,7 +1094,7 @@ impl Fs for FakeFs {
|
|||
e.insert(Arc::new(Mutex::new(FakeFsEntry::File {
|
||||
inode,
|
||||
mtime,
|
||||
content: String::new(),
|
||||
content: Vec::new(),
|
||||
})))
|
||||
.clone(),
|
||||
)),
|
||||
|
@ -1079,35 +1173,30 @@ impl Fs for FakeFs {
|
|||
}
|
||||
|
||||
async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>> {
|
||||
let text = self.load(path).await?;
|
||||
Ok(Box::new(io::Cursor::new(text)))
|
||||
let bytes = self.load_internal(path).await?;
|
||||
Ok(Box::new(io::Cursor::new(bytes)))
|
||||
}
|
||||
|
||||
async fn load(&self, path: &Path) -> Result<String> {
|
||||
let path = normalize_path(path);
|
||||
self.simulate_random_delay().await;
|
||||
let state = self.state.lock();
|
||||
let entry = state.read_path(&path)?;
|
||||
let entry = entry.lock();
|
||||
entry.file_content(&path).cloned()
|
||||
let content = self.load_internal(path).await?;
|
||||
Ok(String::from_utf8(content.clone())?)
|
||||
}
|
||||
|
||||
async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
|
||||
self.simulate_random_delay().await;
|
||||
let path = normalize_path(path.as_path());
|
||||
self.write_file_internal(path, data.to_string())?;
|
||||
|
||||
self.write_file_internal(path, data.into_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> {
|
||||
self.simulate_random_delay().await;
|
||||
let path = normalize_path(path);
|
||||
let content = chunks(text, line_ending).collect();
|
||||
let content = chunks(text, line_ending).collect::<String>();
|
||||
if let Some(path) = path.parent() {
|
||||
self.create_dir(path).await?;
|
||||
}
|
||||
self.write_file_internal(path, content)?;
|
||||
self.write_file_internal(path, content.into_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue