Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'crates/vfs/src/lib.rs')
| -rw-r--r-- | crates/vfs/src/lib.rs | 121 |
1 files changed, 75 insertions, 46 deletions
diff --git a/crates/vfs/src/lib.rs b/crates/vfs/src/lib.rs index ddd6bbe3f7..b07e97cd6c 100644 --- a/crates/vfs/src/lib.rs +++ b/crates/vfs/src/lib.rs @@ -46,7 +46,7 @@ pub mod loader; mod path_interner; mod vfs_path; -use std::{fmt, mem}; +use std::{fmt, hash::BuildHasherDefault, mem}; use crate::path_interner::PathInterner; @@ -54,8 +54,13 @@ pub use crate::{ anchored_path::{AnchoredPath, AnchoredPathBuf}, vfs_path::VfsPath, }; +use indexmap::{map::Entry, IndexMap}; pub use paths::{AbsPath, AbsPathBuf}; +use rustc_hash::FxHasher; +use stdx::hash_once; +use tracing::{span, Level}; + /// Handle to a file in [`Vfs`] /// /// Most functions in rust-analyzer use this when they need to refer to a file. @@ -91,20 +96,13 @@ impl nohash_hasher::IsEnabled for FileId {} pub struct Vfs { interner: PathInterner, data: Vec<FileState>, - // FIXME: This should be a HashMap<FileId, ChangeFile> - // right now we do a nasty deduplication in GlobalState::process_changes that would be a lot - // easier to handle here on insertion. - changes: Vec<ChangedFile>, - // The above FIXME would then also get rid of this probably - created_this_cycle: Vec<FileId>, + changes: IndexMap<FileId, ChangedFile, BuildHasherDefault<FxHasher>>, } #[derive(Copy, Clone, Debug, PartialEq, PartialOrd)] pub enum FileState { - /// The file has been created this cycle. - Created, - /// The file exists. - Exists, + /// The file exists with the given content hash. + Exists(u64), /// The file is deleted. Deleted, } @@ -127,23 +125,23 @@ impl ChangedFile { /// Returns `true` if the change is [`Create`](ChangeKind::Create) or /// [`Delete`](Change::Delete). pub fn is_created_or_deleted(&self) -> bool { - matches!(self.change, Change::Create(_) | Change::Delete) + matches!(self.change, Change::Create(_, _) | Change::Delete) } /// Returns `true` if the change is [`Create`](ChangeKind::Create). pub fn is_created(&self) -> bool { - matches!(self.change, Change::Create(_)) + matches!(self.change, Change::Create(_, _)) } /// Returns `true` if the change is [`Modify`](ChangeKind::Modify). pub fn is_modified(&self) -> bool { - matches!(self.change, Change::Modify(_)) + matches!(self.change, Change::Modify(_, _)) } pub fn kind(&self) -> ChangeKind { match self.change { - Change::Create(_) => ChangeKind::Create, - Change::Modify(_) => ChangeKind::Modify, + Change::Create(_, _) => ChangeKind::Create, + Change::Modify(_, _) => ChangeKind::Modify, Change::Delete => ChangeKind::Delete, } } @@ -153,9 +151,9 @@ impl ChangedFile { #[derive(Eq, PartialEq, Debug)] pub enum Change { /// The file was (re-)created - Create(Vec<u8>), + Create(Vec<u8>, u64), /// The file was modified - Modify(Vec<u8>), + Modify(Vec<u8>, u64), /// The file was deleted Delete, } @@ -174,9 +172,7 @@ pub enum ChangeKind { impl Vfs { /// Id of the given path if it exists in the `Vfs` and is not deleted. pub fn file_id(&self, path: &VfsPath) -> Option<FileId> { - self.interner - .get(path) - .filter(|&it| matches!(self.get(it), FileState::Exists | FileState::Created)) + self.interner.get(path).filter(|&it| matches!(self.get(it), FileState::Exists(_))) } /// File path corresponding to the given `file_id`. @@ -194,9 +190,7 @@ impl Vfs { pub fn iter(&self) -> impl Iterator<Item = (FileId, &VfsPath)> + '_ { (0..self.data.len()) .map(|it| FileId(it as u32)) - .filter(move |&file_id| { - matches!(self.get(file_id), FileState::Exists | FileState::Created) - }) + .filter(move |&file_id| matches!(self.get(file_id), FileState::Exists(_))) .map(move |file_id| { let path = self.interner.lookup(file_id); (file_id, path) @@ -210,44 +204,79 @@ impl Vfs { /// If the path does not currently exists in the `Vfs`, allocates a new /// [`FileId`] for it. pub fn set_file_contents(&mut self, path: VfsPath, contents: Option<Vec<u8>>) -> bool { + let _p = span!(Level::INFO, "Vfs::set_file_contents").entered(); let file_id = self.alloc_file_id(path); let state = self.get(file_id); let change_kind = match (state, contents) { (FileState::Deleted, None) => return false, - (FileState::Deleted, Some(v)) => Change::Create(v), - (FileState::Exists | FileState::Created, None) => Change::Delete, - (FileState::Exists | FileState::Created, Some(v)) => Change::Modify(v), - }; - self.data[file_id.0 as usize] = match change_kind { - Change::Create(_) => { - self.created_this_cycle.push(file_id); - FileState::Created + (FileState::Deleted, Some(v)) => { + let hash = hash_once::<FxHasher>(&*v); + Change::Create(v, hash) + } + (FileState::Exists(_), None) => Change::Delete, + (FileState::Exists(hash), Some(v)) => { + let new_hash = hash_once::<FxHasher>(&*v); + if new_hash == hash { + return false; + } + Change::Modify(v, new_hash) } - // If the file got created this cycle, make sure we keep it that way even - // if a modify comes in - Change::Modify(_) if matches!(state, FileState::Created) => FileState::Created, - Change::Modify(_) => FileState::Exists, - Change::Delete => FileState::Deleted, }; + + let mut set_data = |change_kind| { + self.data[file_id.0 as usize] = match change_kind { + &Change::Create(_, hash) | &Change::Modify(_, hash) => FileState::Exists(hash), + Change::Delete => FileState::Deleted, + }; + }; + let changed_file = ChangedFile { file_id, change: change_kind }; - self.changes.push(changed_file); + match self.changes.entry(file_id) { + // two changes to the same file in one cycle, merge them appropriately + Entry::Occupied(mut o) => { + use Change::*; + + match (&mut o.get_mut().change, changed_file.change) { + // newer `Delete` wins + (change, Delete) => *change = Delete, + // merge `Create` with `Create` or `Modify` + (Create(prev, old_hash), Create(new, new_hash) | Modify(new, new_hash)) => { + *prev = new; + *old_hash = new_hash; + } + // collapse identical `Modify`es + (Modify(prev, old_hash), Modify(new, new_hash)) => { + *prev = new; + *old_hash = new_hash; + } + // equivalent to `Modify` + (change @ Delete, Create(new, new_hash)) => { + *change = Modify(new, new_hash); + } + // shouldn't occur, but collapse into `Create` + (change @ Delete, Modify(new, new_hash)) => { + stdx::never!(); + *change = Create(new, new_hash); + } + // shouldn't occur, but keep the Create + (prev @ Modify(_, _), new @ Create(_, _)) => *prev = new, + } + set_data(&o.get().change); + } + Entry::Vacant(v) => set_data(&v.insert(changed_file).change), + }; + true } /// Drain and returns all the changes in the `Vfs`. - pub fn take_changes(&mut self) -> Vec<ChangedFile> { - for file_id in self.created_this_cycle.drain(..) { - if self.data[file_id.0 as usize] == FileState::Created { - // downgrade the file from `Created` to `Exists` as the cycle is done - self.data[file_id.0 as usize] = FileState::Exists; - } - } + pub fn take_changes(&mut self) -> IndexMap<FileId, ChangedFile, BuildHasherDefault<FxHasher>> { mem::take(&mut self.changes) } /// Provides a panic-less way to verify file_id validity. pub fn exists(&self, file_id: FileId) -> bool { - matches!(self.get(file_id), FileState::Exists | FileState::Created) + matches!(self.get(file_id), FileState::Exists(_)) } /// Returns the id associated with `path` |