Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion crates/vfs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ utils.workspace = true

anyhow.workspace = true
tracing.workspace = true
glob = "*"

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
crossbeam-channel.workspace = true
notify.workspace = true
glob = "*"
same-file = "*"
28 changes: 26 additions & 2 deletions crates/vfs/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
// Some parts are copied from rust-analyzer

mod glob_abs_path;

#[cfg(not(target_family = "wasm"))]
mod local_fs;

#[cfg(target_family = "wasm")]
#[path = "local_fs_stub.rs"]
mod local_fs;

mod memory;
mod normalized_path;
mod path;
mod tree;
Expand All @@ -10,18 +18,34 @@ mod workspaces;

use std::{borrow::Cow, path::Path, sync::Arc};

use crossbeam_channel::Receiver;

pub use glob_abs_path::GlobAbsPath;

pub use local_fs::{LocalFS, SimpleLocalFS};

#[cfg(target_family = "wasm")]
pub use memory::InMemoryFs;

pub use normalized_path::NormalizedPath;
pub use path::AbsPath;
pub use tree::{DirOrFile, Directory, DirectoryEntry, Entries, FileEntry, FileIndex, Parent};
pub use vfs::{InvalidationResult, PathWithScheme, Vfs, VfsFile, VfsPanicRecovery};
pub use workspaces::{Workspace, WorkspaceKind};

#[cfg(not(target_family = "wasm"))]
pub use crossbeam_channel::Receiver;

#[cfg(not(target_family = "wasm"))]
pub type NotifyEvent = notify::Result<notify::Event>;

#[cfg(target_family = "wasm")]
pub type NotifyEvent = ();

#[cfg(target_family = "wasm")]
use std::marker::PhantomData;

#[cfg(target_family = "wasm")]
pub type Receiver<T> = PhantomData<T>;

/// Interface for reading and watching files.
pub trait VfsHandler: Sync + Send {
/// Load the content of the given file, returning [`None`] if it does not
Expand Down
83 changes: 83 additions & 0 deletions crates/vfs/src/local_fs_stub.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
use std::{
path::{Path, PathBuf},
sync::Arc,
};

use crate::{
AbsPath, DirectoryEntry, Entries, NormalizedPath, NotifyEvent, Parent, PathWithScheme,
VfsHandler,
};

pub type SimpleLocalFS = LocalFS;

#[derive(Clone, Default)]
pub struct LocalFS;

impl LocalFS {
pub fn without_watcher() -> Self {
Self
}
pub fn with_watcher<T>(_: T) -> Self {
Self
}
pub fn watch<P: AsRef<Path>>(&self, _: P) {}
pub fn current_dir(&self) -> Arc<AbsPath> {
self.unchecked_abs_path("")
}
pub fn normalized_path_from_current_dir(&self, p: &str) -> Arc<NormalizedPath> {
self.normalize_rc_path(self.absolute_path(&self.current_dir(), p))
}
}

impl VfsHandler for LocalFS {
fn read_and_watch_file(&self, _: &PathWithScheme) -> Option<String> {
None
}
fn notify_receiver(&self) -> Option<&crate::Receiver<NotifyEvent>> {
None
}
fn on_invalidated_in_memory_file(&self, _: PathWithScheme) {}
fn read_and_watch_dir(&self, _: &str, _: Parent) -> Entries {
Entries::from_vec(vec![])
}
fn read_and_watch_entry(&self, _: &str, _: Parent, _: &str) -> Option<DirectoryEntry> {
None
}
fn separator(&self) -> char {
'/'
}
fn split_off_folder<'a>(&self, path: &'a str) -> (&'a str, Option<&'a str>) {
if path.is_empty() {
return (path, None);
}
if let Some(pos) = path
.as_bytes()
.iter()
.position(|b| *b == b'/' || *b == b'\\')
{
let (head, tail) = path.split_at(pos);
(
head,
if tail.len() > 1 {
Some(&tail[1..])
} else {
None
},
)
} else {
(path, None)
}
}
fn join(&self, path: &AbsPath, name: &str) -> Arc<AbsPath> {
let p = Path::new(&**path);
let joined = if p.as_os_str().is_empty() {
PathBuf::from(name)
} else {
p.join(name)
};
self.unchecked_abs_path(joined.to_str().unwrap_or(name))
}
fn is_case_sensitive(&self) -> bool {
true
}
}
205 changes: 205 additions & 0 deletions crates/vfs/src/memory.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
use std::{
collections::{BTreeSet, HashMap},
path::{Component, Path, PathBuf},
sync::{Arc, RwLock},
};

use crate::{
AbsPath, Directory, DirectoryEntry, Entries, FileEntry, NotifyEvent, Parent, PathWithScheme,
Receiver, VfsHandler,
};

#[derive(Clone, Default)]
pub struct InMemoryFs {
inner: Arc<Inner>,
}

#[derive(Default)]
struct Inner {
files: RwLock<HashMap<String, String>>,
separator: char,
}

impl InMemoryFs {
pub fn new() -> Self {
Self {
inner: Arc::new(Inner {
files: Default::default(),
separator: '/',
}),
}
}

pub fn set_file<S: AsRef<str>, C: Into<String>>(&self, path: S, contents: C) {
let key = normalize_key(path.as_ref());
self.inner
.files
.write()
.unwrap()
.insert(key, contents.into());
}

pub fn remove_file<S: AsRef<str>>(&self, path: S) {
let key = normalize_key(path.as_ref());
self.inner.files.write().unwrap().remove(&key);
}

pub fn read_file<S: AsRef<str>>(&self, path: S) -> Option<String> {
let key = normalize_key(path.as_ref());
self.inner.files.read().unwrap().get(&key).cloned()
}

pub fn clear(&self) {
self.inner.files.write().unwrap().clear();
}

fn separator(&self) -> char {
self.inner.separator
}

fn join_path(&self, base: &AbsPath, name: &str) -> PathBuf {
let base_path = Path::new(&**base);
if base_path.as_os_str().is_empty() {
PathBuf::from(name)
} else {
base_path.join(name)
}
}
}

impl VfsHandler for InMemoryFs {
fn read_and_watch_file(&self, path: &PathWithScheme) -> Option<String> {
self.read_file(path.path.to_string())
}

fn notify_receiver(&self) -> Option<&Receiver<NotifyEvent>> {
None
}

fn on_invalidated_in_memory_file(&self, _path: PathWithScheme) {}

fn read_and_watch_dir(&self, path: &str, parent: Parent) -> Entries {
let base = normalize_key(path);
let mut file_children = Vec::new();
let mut dir_children: BTreeSet<String> = BTreeSet::new();

let prefix = if base.is_empty() {
String::new()
} else {
format!("{base}/")
};

for key in self.inner.files.read().unwrap().keys() {
if base.is_empty() {
if let Some((first, rest)) = key.split_once('/') {
if rest.is_empty() {
file_children.push(first.to_string());
} else {
dir_children.insert(first.to_string());
}
} else if !key.is_empty() {
file_children.push(key.clone());
}
} else if key == &base {
file_children.push(key.clone());
} else if let Some(stripped) = key.strip_prefix(&prefix) {
if let Some((segment, rest)) = stripped.split_once('/') {
if rest.is_empty() {
file_children.push(segment.to_string());
} else {
dir_children.insert(segment.to_string());
}
} else if !stripped.is_empty() {
file_children.push(stripped.to_string());
}
}
}

file_children.sort();

let mut entries = Vec::with_capacity(dir_children.len() + file_children.len());
for dir in dir_children {
entries.push(DirectoryEntry::Directory(Directory::new(
parent.clone(),
dir.into_boxed_str(),
)));
}
for file in file_children {
entries.push(DirectoryEntry::File(FileEntry::new(
parent.clone(),
file.into_boxed_str(),
)));
}
Entries::from_vec(entries)
}

fn read_and_watch_entry(
&self,
path: &str,
parent: Parent,
replace_name: &str,
) -> Option<DirectoryEntry> {
let key = normalize_key(path);
let files = self.inner.files.read().unwrap();
if files.contains_key(&key) {
return Some(DirectoryEntry::File(FileEntry::new(
parent,
replace_name.into(),
)));
}
let prefix = format!("{key}/");
if files.keys().any(|candidate| candidate.starts_with(&prefix)) {
return Some(DirectoryEntry::Directory(Directory::new(
parent,
replace_name.into(),
)));
}
None
}

fn separator(&self) -> char {
self.separator()
}

fn split_off_folder<'a>(&self, path: &'a str) -> (&'a str, Option<&'a str>) {
if path.is_empty() {
return (path, None);
}
let bytes = path.as_bytes();
if let Some(pos) = bytes.iter().position(|b| *b == b'/' || *b == b'\\') {
let (head, tail) = path.split_at(pos);
let tail = &tail[1..];
(head, if tail.is_empty() { None } else { Some(tail) })
} else {
(path, None)
}
}

fn join(&self, path: &AbsPath, name: &str) -> Arc<AbsPath> {
let joined = self.join_path(path, name);
let joined = joined.to_str().unwrap_or(name);
self.unchecked_abs_path(joined)
}

fn is_case_sensitive(&self) -> bool {
true
}
}

fn normalize_key(path: &str) -> String {
let mut buf = PathBuf::new();
for component in Path::new(path).components() {
match component {
Component::CurDir => {}
Component::ParentDir => {
buf.pop();
}
other => buf.push(other.as_os_str()),
}
}
let normalized = buf
.to_str()
.map(|s| s.replace('\\', "/"))
.unwrap_or_else(|| path.replace('\\', "/"));
normalized.trim_matches('/').to_string()
}
2 changes: 2 additions & 0 deletions crates/zuban_python/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ serde_json.workspace = true
tracing.workspace = true
lsp-types.workspace = true
rayon.workspace = true

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
which = "*"

[dev-dependencies]
Expand Down
Loading