Detailed changes
@@ -861,7 +861,7 @@ struct FakeFsState {
next_inode: u64,
next_mtime: SystemTime,
git_event_tx: smol::channel::Sender<PathBuf>,
- event_txs: Vec<smol::channel::Sender<Vec<PathEvent>>>,
+ event_txs: Vec<(PathBuf, smol::channel::Sender<Vec<PathEvent>>)>,
events_paused: bool,
buffered_events: Vec<PathEvent>,
metadata_call_count: usize,
@@ -1013,7 +1013,7 @@ impl FakeFsState {
fn flush_events(&mut self, mut count: usize) {
count = count.min(self.buffered_events.len());
let events = self.buffered_events.drain(0..count).collect::<Vec<_>>();
- self.event_txs.retain(|tx| {
+ self.event_txs.retain(|(_, tx)| {
let _ = tx.try_send(events.clone());
!tx.is_closed()
});
@@ -1112,7 +1112,7 @@ impl FakeFs {
}
pub async fn insert_file(&self, path: impl AsRef<Path>, content: Vec<u8>) {
- self.write_file_internal(path, content).unwrap()
+ self.write_file_internal(path, content, true).unwrap()
}
pub async fn insert_symlink(&self, path: impl AsRef<Path>, target: PathBuf) {
@@ -1134,30 +1134,50 @@ impl FakeFs {
state.emit_event([(path, None)]);
}
- fn write_file_internal(&self, path: impl AsRef<Path>, content: Vec<u8>) -> Result<()> {
+ fn write_file_internal(
+ &self,
+ path: impl AsRef<Path>,
+ new_content: Vec<u8>,
+ recreate_inode: bool,
+ ) -> Result<()> {
let mut state = self.state.lock();
- let file = Arc::new(Mutex::new(FakeFsEntry::File {
- inode: state.get_and_increment_inode(),
- mtime: state.get_and_increment_mtime(),
- len: content.len() as u64,
- content,
- }));
+ let new_inode = state.get_and_increment_inode();
+ let new_mtime = state.get_and_increment_mtime();
+ let new_len = new_content.len() as u64;
let mut kind = None;
- state.write_path(path.as_ref(), {
- let kind = &mut kind;
- move |entry| {
- match entry {
- btree_map::Entry::Vacant(e) => {
- *kind = Some(PathEventKind::Created);
- e.insert(file);
- }
- btree_map::Entry::Occupied(mut e) => {
- *kind = Some(PathEventKind::Changed);
- *e.get_mut() = file;
+ state.write_path(path.as_ref(), |entry| {
+ match entry {
+ btree_map::Entry::Vacant(e) => {
+ kind = Some(PathEventKind::Created);
+ e.insert(Arc::new(Mutex::new(FakeFsEntry::File {
+ inode: new_inode,
+ mtime: new_mtime,
+ len: new_len,
+ content: new_content,
+ })));
+ }
+ btree_map::Entry::Occupied(mut e) => {
+ kind = Some(PathEventKind::Changed);
+ if let FakeFsEntry::File {
+ inode,
+ mtime,
+ len,
+ content,
+ ..
+ } = &mut *e.get_mut().lock()
+ {
+ *mtime = new_mtime;
+ *content = new_content;
+ *len = new_len;
+ if recreate_inode {
+ *inode = new_inode;
+ }
+ } else {
+ anyhow::bail!("not a file")
}
}
- Ok(())
}
+ Ok(())
})?;
state.emit_event([(path.as_ref(), kind)]);
Ok(())
@@ -1589,6 +1609,15 @@ impl FakeFs {
self.state.lock().read_dir_call_count
}
+ pub fn watched_paths(&self) -> Vec<PathBuf> {
+ let state = self.state.lock();
+ state
+ .event_txs
+ .iter()
+ .filter_map(|(path, tx)| Some(path.clone()).filter(|_| !tx.is_closed()))
+ .collect()
+ }
+
/// How many `metadata` calls have been issued.
pub fn metadata_call_count(&self) -> usize {
self.state.lock().metadata_call_count
@@ -1765,7 +1794,7 @@ impl Fs for FakeFs {
) -> Result<()> {
let mut bytes = Vec::new();
content.read_to_end(&mut bytes).await?;
- self.write_file_internal(path, bytes)?;
+ self.write_file_internal(path, bytes, true)?;
Ok(())
}
@@ -1782,7 +1811,7 @@ impl Fs for FakeFs {
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)?;
+ self.write_file_internal(&path, bytes, true)?;
}
}
Ok(())
@@ -1976,7 +2005,7 @@ impl Fs for FakeFs {
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.into_bytes())?;
+ self.write_file_internal(path, data.into_bytes(), true)?;
Ok(())
}
@@ -1987,7 +2016,7 @@ impl Fs for FakeFs {
if let Some(path) = path.parent() {
self.create_dir(path).await?;
}
- self.write_file_internal(path, content.into_bytes())?;
+ self.write_file_internal(path, content.into_bytes(), false)?;
Ok(())
}
@@ -2107,8 +2136,8 @@ impl Fs for FakeFs {
) {
self.simulate_random_delay().await;
let (tx, rx) = smol::channel::unbounded();
- self.state.lock().event_txs.push(tx);
let path = path.to_path_buf();
+ self.state.lock().event_txs.push((path.clone(), tx));
let executor = self.executor.clone();
(
Box::pin(futures::StreamExt::filter(rx, move |events| {
@@ -3026,149 +3026,89 @@ impl LocalLspStore {
language_server_id
);
- enum PathToWatch {
- Worktree {
- literal_prefix: Arc<Path>,
- pattern: String,
- },
- Absolute {
- path: Arc<Path>,
- pattern: String,
- },
- }
for watcher in watchers {
- let mut found_host = false;
- for worktree in &worktrees {
- let glob_is_inside_worktree = worktree.update(cx, |tree, _| {
- let worktree_root_path = tree.abs_path();
- let path_to_watch = match &watcher.glob_pattern {
- lsp::GlobPattern::String(s) => {
- let watcher_path = SanitizedPath::from(s);
- match watcher_path.as_path().strip_prefix(&worktree_root_path) {
- Ok(relative) => {
- let pattern = relative.to_string_lossy().to_string();
- let literal_prefix = glob_literal_prefix(relative).into();
-
- PathToWatch::Worktree {
- literal_prefix,
- pattern,
- }
- }
- Err(_) => {
- let path = glob_literal_prefix(watcher_path.as_path());
- let pattern = watcher_path
- .as_path()
- .strip_prefix(&path)
- .map(|p| p.to_string_lossy().to_string())
- .unwrap_or_else(|e| {
- debug_panic!(
- "Failed to strip prefix for string pattern: {}, with prefix: {}, with error: {}",
- s,
- path.display(),
- e
- );
- watcher_path.as_path().to_string_lossy().to_string()
- });
- let path = if path.components().next().is_none() {
- worktree_root_path.clone()
- } else {
- path.into()
- };
-
- PathToWatch::Absolute { path, pattern }
- }
- }
+ if let Some((worktree, literal_prefix, pattern)) =
+ self.worktree_and_path_for_file_watcher(&worktrees, &watcher, cx)
+ {
+ worktree.update(cx, |worktree, _| {
+ if let Some((tree, glob)) =
+ worktree.as_local_mut().zip(Glob::new(&pattern).log_err())
+ {
+ tree.add_path_prefix_to_scan(literal_prefix.into());
+ worktree_globs
+ .entry(tree.id())
+ .or_insert_with(GlobSetBuilder::new)
+ .add(glob);
+ }
+ });
+ } else {
+ let (path, pattern) = match &watcher.glob_pattern {
+ lsp::GlobPattern::String(s) => {
+ let watcher_path = SanitizedPath::from(s);
+ let path = glob_literal_prefix(watcher_path.as_path());
+ let pattern = watcher_path
+ .as_path()
+ .strip_prefix(&path)
+ .map(|p| p.to_string_lossy().to_string())
+ .unwrap_or_else(|e| {
+ debug_panic!(
+ "Failed to strip prefix for string pattern: {}, with prefix: {}, with error: {}",
+ s,
+ path.display(),
+ e
+ );
+ watcher_path.as_path().to_string_lossy().to_string()
+ });
+ (path, pattern)
+ }
+ lsp::GlobPattern::Relative(rp) => {
+ let Ok(mut base_uri) = match &rp.base_uri {
+ lsp::OneOf::Left(workspace_folder) => &workspace_folder.uri,
+ lsp::OneOf::Right(base_uri) => base_uri,
}
- lsp::GlobPattern::Relative(rp) => {
- let Ok(mut base_uri) = match &rp.base_uri {
- lsp::OneOf::Left(workspace_folder) => &workspace_folder.uri,
- lsp::OneOf::Right(base_uri) => base_uri,
- }
- .to_file_path() else {
- return false;
- };
-
- match base_uri.strip_prefix(&worktree_root_path) {
- Ok(relative) => {
- let mut literal_prefix = relative.to_owned();
- literal_prefix
- .push(glob_literal_prefix(Path::new(&rp.pattern)));
+ .to_file_path() else {
+ continue;
+ };
- PathToWatch::Worktree {
- literal_prefix: literal_prefix.into(),
- pattern: rp.pattern.clone(),
- }
- }
- Err(_) => {
- let path = glob_literal_prefix(Path::new(&rp.pattern));
- let pattern = Path::new(&rp.pattern)
- .strip_prefix(&path)
- .map(|p| p.to_string_lossy().to_string())
- .unwrap_or_else(|e| {
- debug_panic!(
- "Failed to strip prefix for relative pattern: {}, with prefix: {}, with error: {}",
- rp.pattern,
- path.display(),
- e
- );
- rp.pattern.clone()
- });
- base_uri.push(path);
+ let path = glob_literal_prefix(Path::new(&rp.pattern));
+ let pattern = Path::new(&rp.pattern)
+ .strip_prefix(&path)
+ .map(|p| p.to_string_lossy().to_string())
+ .unwrap_or_else(|e| {
+ debug_panic!(
+ "Failed to strip prefix for relative pattern: {}, with prefix: {}, with error: {}",
+ rp.pattern,
+ path.display(),
+ e
+ );
+ rp.pattern.clone()
+ });
+ base_uri.push(path);
+ (base_uri, pattern)
+ }
+ };
- let path = if base_uri.components().next().is_none() {
- debug_panic!("base_uri is empty, {}", base_uri.display());
- worktree_root_path.clone()
- } else {
- base_uri.into()
- };
- PathToWatch::Absolute { path, pattern }
- }
- }
- }
- };
- match path_to_watch {
- PathToWatch::Worktree {
- literal_prefix,
- pattern,
- } => {
- if let Some((tree, glob)) =
- tree.as_local_mut().zip(Glob::new(&pattern).log_err())
- {
- tree.add_path_prefix_to_scan(literal_prefix);
- worktree_globs
- .entry(tree.id())
- .or_insert_with(GlobSetBuilder::new)
- .add(glob);
- } else {
- return false;
- }
- }
- PathToWatch::Absolute { path, pattern } => {
- if let Some(glob) = Glob::new(&pattern).log_err() {
- abs_globs
- .entry(path)
- .or_insert_with(GlobSetBuilder::new)
- .add(glob);
- }
+ if let Some(glob) = Glob::new(&pattern).log_err() {
+ if !path
+ .components()
+ .any(|c| matches!(c, path::Component::Normal(_)))
+ {
+ // For an unrooted glob like `**/Cargo.toml`, watch it within each worktree,
+ // rather than adding a new watcher for `/`.
+ for worktree in &worktrees {
+ worktree_globs
+ .entry(worktree.read(cx).id())
+ .or_insert_with(GlobSetBuilder::new)
+ .add(glob.clone());
}
+ } else {
+ abs_globs
+ .entry(path.into())
+ .or_insert_with(GlobSetBuilder::new)
+ .add(glob);
}
- true
- });
- if glob_is_inside_worktree {
- log::trace!(
- "Watcher pattern `{}` has been attached to the worktree at `{}`",
- serde_json::to_string(&watcher.glob_pattern).unwrap(),
- worktree.read(cx).abs_path().display()
- );
- found_host = true;
}
}
- if !found_host {
- log::error!(
- "Watcher pattern `{}` has not been attached to any worktree or absolute path",
- serde_json::to_string(&watcher.glob_pattern).unwrap()
- )
- }
}
let mut watch_builder = LanguageServerWatchedPathsBuilder::default();
@@ -3185,6 +3125,45 @@ impl LocalLspStore {
watch_builder
}
+ fn worktree_and_path_for_file_watcher(
+ &self,
+ worktrees: &[Entity<Worktree>],
+ watcher: &FileSystemWatcher,
+ cx: &App,
+ ) -> Option<(Entity<Worktree>, PathBuf, String)> {
+ worktrees.iter().find_map(|worktree| {
+ let tree = worktree.read(cx);
+ let worktree_root_path = tree.abs_path();
+ match &watcher.glob_pattern {
+ lsp::GlobPattern::String(s) => {
+ let watcher_path = SanitizedPath::from(s);
+ let relative = watcher_path
+ .as_path()
+ .strip_prefix(&worktree_root_path)
+ .ok()?;
+ let literal_prefix = glob_literal_prefix(relative);
+ Some((
+ worktree.clone(),
+ literal_prefix,
+ relative.to_string_lossy().to_string(),
+ ))
+ }
+ lsp::GlobPattern::Relative(rp) => {
+ let base_uri = match &rp.base_uri {
+ lsp::OneOf::Left(workspace_folder) => &workspace_folder.uri,
+ lsp::OneOf::Right(base_uri) => base_uri,
+ }
+ .to_file_path()
+ .ok()?;
+ let relative = base_uri.strip_prefix(&worktree_root_path).ok()?;
+ let mut literal_prefix = relative.to_owned();
+ literal_prefix.push(glob_literal_prefix(Path::new(&rp.pattern)));
+ Some((worktree.clone(), literal_prefix, rp.pattern.clone()))
+ }
+ }
+ })
+ }
+
fn rebuild_watched_paths(
&mut self,
language_server_id: LanguageServerId,
@@ -27,7 +27,7 @@ use lsp::{
WillRenameFiles, notification::DidRenameFiles,
};
use parking_lot::Mutex;
-use paths::tasks_file;
+use paths::{config_dir, tasks_file};
use postage::stream::Stream as _;
use pretty_assertions::{assert_eq, assert_matches};
use serde_json::json;
@@ -919,6 +919,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
path!("/the-root"),
json!({
".gitignore": "target\n",
+ "Cargo.lock": "",
"src": {
"a.rs": "",
"b.rs": "",
@@ -943,9 +944,37 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
}),
)
.await;
+ fs.insert_tree(
+ path!("/the-registry"),
+ json!({
+ "dep1": {
+ "src": {
+ "dep1.rs": "",
+ }
+ },
+ "dep2": {
+ "src": {
+ "dep2.rs": "",
+ }
+ },
+ }),
+ )
+ .await;
+ fs.insert_tree(
+ path!("/the/stdlib"),
+ json!({
+ "LICENSE": "",
+ "src": {
+ "string.rs": "",
+ }
+ }),
+ )
+ .await;
let project = Project::test(fs.clone(), [path!("/the-root").as_ref()], cx).await;
- let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+ let (language_registry, lsp_store) = project.read_with(cx, |project, _| {
+ (project.languages().clone(), project.lsp_store())
+ });
language_registry.add(rust_lang());
let mut fake_servers = language_registry.register_fake_lsp(
"Rust",
@@ -978,6 +1007,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
&[
(Path::new(""), false),
(Path::new(".gitignore"), false),
+ (Path::new("Cargo.lock"), false),
(Path::new("src"), false),
(Path::new("src/a.rs"), false),
(Path::new("src/b.rs"), false),
@@ -988,8 +1018,26 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
let prev_read_dir_count = fs.read_dir_call_count();
- // Keep track of the FS events reported to the language server.
let fake_server = fake_servers.next().await.unwrap();
+ let (server_id, server_name) = lsp_store.read_with(cx, |lsp_store, _| {
+ let (id, status) = lsp_store.language_server_statuses().next().unwrap();
+ (id, LanguageServerName::from(status.name.as_str()))
+ });
+
+ // Simulate jumping to a definition in a dependency outside of the worktree.
+ let _out_of_worktree_buffer = project
+ .update(cx, |project, cx| {
+ project.open_local_buffer_via_lsp(
+ lsp::Url::from_file_path(path!("/the-registry/dep1/src/dep1.rs")).unwrap(),
+ server_id,
+ server_name.clone(),
+ cx,
+ )
+ })
+ .await
+ .unwrap();
+
+ // Keep track of the FS events reported to the language server.
let file_changes = Arc::new(Mutex::new(Vec::new()));
fake_server
.request::<lsp::request::RegisterCapability>(lsp::RegistrationParams {
@@ -1017,6 +1065,18 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
),
kind: None,
},
+ lsp::FileSystemWatcher {
+ glob_pattern: lsp::GlobPattern::String(
+ path!("/the/stdlib/src/**/*.rs").to_string(),
+ ),
+ kind: None,
+ },
+ lsp::FileSystemWatcher {
+ glob_pattern: lsp::GlobPattern::String(
+ path!("**/Cargo.lock").to_string(),
+ ),
+ kind: None,
+ },
],
},
)
@@ -1036,12 +1096,23 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
cx.executor().run_until_parked();
assert_eq!(mem::take(&mut *file_changes.lock()), &[]);
- assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 4);
+ assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 5);
+
+ let mut new_watched_paths = fs.watched_paths();
+ new_watched_paths.retain(|path| !path.starts_with(config_dir()));
+ assert_eq!(
+ &new_watched_paths,
+ &[
+ Path::new(path!("/the-root")),
+ Path::new(path!("/the-registry/dep1/src/dep1.rs")),
+ Path::new(path!("/the/stdlib/src"))
+ ]
+ );
// Now the language server has asked us to watch an ignored directory path,
// so we recursively load it.
project.update(cx, |project, cx| {
- let worktree = project.worktrees(cx).next().unwrap();
+ let worktree = project.visible_worktrees(cx).next().unwrap();
assert_eq!(
worktree
.read(cx)
@@ -1052,6 +1123,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
&[
(Path::new(""), false),
(Path::new(".gitignore"), false),
+ (Path::new("Cargo.lock"), false),
(Path::new("src"), false),
(Path::new("src/a.rs"), false),
(Path::new("src/b.rs"), false),
@@ -1088,12 +1160,37 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
)
.await
.unwrap();
+ fs.save(
+ path!("/the-root/Cargo.lock").as_ref(),
+ &"".into(),
+ Default::default(),
+ )
+ .await
+ .unwrap();
+ fs.save(
+ path!("/the-stdlib/LICENSE").as_ref(),
+ &"".into(),
+ Default::default(),
+ )
+ .await
+ .unwrap();
+ fs.save(
+ path!("/the/stdlib/src/string.rs").as_ref(),
+ &"".into(),
+ Default::default(),
+ )
+ .await
+ .unwrap();
// The language server receives events for the FS mutations that match its watch patterns.
cx.executor().run_until_parked();
assert_eq!(
&*file_changes.lock(),
&[
+ lsp::FileEvent {
+ uri: lsp::Url::from_file_path(path!("/the-root/Cargo.lock")).unwrap(),
+ typ: lsp::FileChangeType::CHANGED,
+ },
lsp::FileEvent {
uri: lsp::Url::from_file_path(path!("/the-root/src/b.rs")).unwrap(),
typ: lsp::FileChangeType::DELETED,
@@ -1106,6 +1203,10 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
uri: lsp::Url::from_file_path(path!("/the-root/target/y/out/y2.rs")).unwrap(),
typ: lsp::FileChangeType::CREATED,
},
+ lsp::FileEvent {
+ uri: lsp::Url::from_file_path(path!("/the/stdlib/src/string.rs")).unwrap(),
+ typ: lsp::FileChangeType::CHANGED,
+ },
]
);
}