mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 16:08:05 +00:00
fix(project-tree): refresh expanded excluded folders when files change
The watcher drops changes inside excluded and ignored folders so the index and version control don't react to build output. That also kept an expanded `target` from showing files a build added or removed. Those folders are now reported in their own event, which only the Project view handles, and only for folders it has loaded. Changes to existing files aren't reported, since the listing stays the same.
This commit is contained in:
@@ -21,6 +21,10 @@ pub enum IdeEvent {
|
||||
modified: Vec<crate::IpcPath>,
|
||||
removed: Vec<crate::IpcPath>,
|
||||
},
|
||||
/// Excluded or ignored folders whose contents changed; they stay out of `FsChanged`.
|
||||
ExcludedDirsChanged {
|
||||
dirs: Vec<crate::IpcPath>,
|
||||
},
|
||||
/// The backend changed a document (reload from disk, formatting, refactoring).
|
||||
/// The UI applies `changes` on top of `base_version`, yielding `version`.
|
||||
DocPatched {
|
||||
|
||||
@@ -310,11 +310,18 @@ pub struct FsBatch {
|
||||
/// refresh as a worktree edit and schedule another `vcs_status` (which
|
||||
/// writes `.git/index` again — an idle refresh loop).
|
||||
pub vcs: Vec<PathBuf>,
|
||||
/// Folders holding changes filtered out as excluded or ignored. Only a view already
|
||||
/// showing such a folder cares, so they don't count as worktree changes.
|
||||
pub excluded_dirs: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
impl FsBatch {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.created.is_empty() && self.modified.is_empty() && self.removed.is_empty() && self.vcs.is_empty()
|
||||
self.created.is_empty()
|
||||
&& self.modified.is_empty()
|
||||
&& self.removed.is_empty()
|
||||
&& self.vcs.is_empty()
|
||||
&& self.excluded_dirs.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -87,6 +87,9 @@ impl Watcher {
|
||||
removed: batch.removed.iter().map(Into::into).collect(),
|
||||
});
|
||||
}
|
||||
if !batch.excluded_dirs.is_empty() {
|
||||
sink.emit(IdeEvent::ExcludedDirsChanged { dirs: batch.excluded_dirs.iter().map(Into::into).collect() });
|
||||
}
|
||||
on_change(batch);
|
||||
};
|
||||
let inner = match mode {
|
||||
@@ -520,6 +523,7 @@ impl EventFilter {
|
||||
}
|
||||
|
||||
let mut batch = FsBatch::default();
|
||||
let mut excluded_dirs = BTreeSet::new();
|
||||
for (path, change) in changes {
|
||||
if change == Change::Vcs {
|
||||
batch.vcs.push(path);
|
||||
@@ -551,10 +555,12 @@ impl EventFilter {
|
||||
// itself — reporting one extra file removal is harmless.
|
||||
let self_excluded = (is_dir || change == Change::Removed)
|
||||
&& (self.excludes.read().is_match(rel) || self.gitignore.is_ignored_self(&path, true));
|
||||
if !self_excluded && self.excludes.read().is_excluded(rel) {
|
||||
continue;
|
||||
}
|
||||
if !self_excluded && self.gitignore.is_ignored(&path, is_dir) {
|
||||
if !self_excluded && (self.excludes.read().is_excluded(rel) || self.gitignore.is_ignored(&path, is_dir))
|
||||
{
|
||||
// Only a file appearing or going changes what the folder lists.
|
||||
if change != Change::Modified {
|
||||
excluded_dirs.extend(path.parent().map(Path::to_path_buf));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -564,6 +570,7 @@ impl EventFilter {
|
||||
_ => batch.modified.push(path),
|
||||
}
|
||||
}
|
||||
batch.excluded_dirs = excluded_dirs.into_iter().collect();
|
||||
(batch, rule_dirs.into_iter().collect())
|
||||
}
|
||||
}
|
||||
@@ -661,7 +668,8 @@ mod tests {
|
||||
ev(create_file, vec![root.join("dist/app.js")]),
|
||||
ev(create_file, vec![root.join("debug.log")]),
|
||||
]);
|
||||
assert!(batch.is_empty(), "{batch:?}");
|
||||
assert!(batch.created.is_empty() && batch.modified.is_empty() && batch.removed.is_empty(), "{batch:?}");
|
||||
assert_eq!(batch.excluded_dirs, vec![root.clone(), root.join("dist"), root.join("target/debug")]);
|
||||
|
||||
// Sibling files in unignored directories still come through.
|
||||
let batch = filter.batch(&[ev(create_file, vec![root.join("src/main.rs")])]);
|
||||
|
||||
@@ -3379,6 +3379,8 @@ export type IdeEvent =
|
||||
{ type: "windowGroupsChanged" } |
|
||||
/** Files or directories changed on disk (after debouncing, ignore rules applied). */
|
||||
{ type: "fsChanged"; created: IpcPath[]; modified: IpcPath[]; removed: IpcPath[] } |
|
||||
/** Excluded or ignored folders whose contents changed; they stay out of `FsChanged`. */
|
||||
{ type: "excludedDirsChanged"; dirs: IpcPath[] } |
|
||||
/**
|
||||
* The backend changed a document (reload from disk, formatting, refactoring).
|
||||
* The UI applies `changes` on top of `base_version`, yielding `version`.
|
||||
|
||||
@@ -22,7 +22,15 @@ import { ProjectHeaderActions, useProjectHeaderMenu, useProjectTitleMenu } from
|
||||
import { loadHomeDir } from "./home";
|
||||
import { selectedNodes } from "./operations";
|
||||
import { ProjectView } from "./ProjectView";
|
||||
import { applyFsChange, initProjectTree, reloadAll, setChangedFiles, setOpenFiles, useProjectTree } from "./store";
|
||||
import {
|
||||
applyFsChange,
|
||||
initProjectTree,
|
||||
reloadAll,
|
||||
reloadLoadedDirs,
|
||||
setChangedFiles,
|
||||
setOpenFiles,
|
||||
useProjectTree,
|
||||
} from "./store";
|
||||
|
||||
export function activate(): () => void {
|
||||
loadHomeDir();
|
||||
@@ -138,6 +146,7 @@ export function activate(): () => void {
|
||||
.map((n) => n.path),
|
||||
),
|
||||
onIdeEvent("fsChanged", (e) => void applyFsChange(e)),
|
||||
onIdeEvent("excludedDirsChanged", (e) => void reloadLoadedDirs(e.dirs)),
|
||||
];
|
||||
return () => {
|
||||
disposers.reverse().forEach((d) => d());
|
||||
|
||||
@@ -3,6 +3,7 @@ import { MemoryFs, setProjectFsForTests } from "./fs";
|
||||
import { visibleInfo } from "./model";
|
||||
import {
|
||||
applyFsChange,
|
||||
reloadLoadedDirs,
|
||||
currentModel,
|
||||
expandAll,
|
||||
initProjectTree,
|
||||
@@ -75,6 +76,20 @@ describe("lazy loading", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("refresh of excluded folders", () => {
|
||||
it("reloads only the folders the tree has loaded", async () => {
|
||||
await loadDir(ROOT);
|
||||
await loadDir("/p/docs");
|
||||
const spy = vi.spyOn(fs, "readDir");
|
||||
|
||||
await reloadLoadedDirs(["/p/docs", "/p/target/never-opened"]);
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
expect(spy).toHaveBeenCalledWith("/p/docs");
|
||||
spy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("refresh on fsChanged", () => {
|
||||
it("reloads affected loaded folders and keeps expansion and selection", async () => {
|
||||
await loadDir(ROOT);
|
||||
|
||||
@@ -451,6 +451,12 @@ export async function applyFsChange(event: { created: string[]; modified: string
|
||||
await Promise.all(plan.reload.map((p) => loadDir(p, true)));
|
||||
}
|
||||
|
||||
/** Re-reads the folders among `dirs` the tree has loaded; the rest stay unread. */
|
||||
export async function reloadLoadedDirs(dirs: readonly string[]) {
|
||||
const { listings } = get();
|
||||
await Promise.all(dirs.filter((p) => listings.has(p)).map((p) => loadDir(p, true)));
|
||||
}
|
||||
|
||||
/** Re-reads every loaded folder (View › Reload from Disk). */
|
||||
export async function reloadAll() {
|
||||
const { listings } = get();
|
||||
|
||||
Reference in New Issue
Block a user