Skip to content

Commit 12a418b

Browse files
committed
feat(vscode): two-tier files.exclude with folder-scoped vault patterns
Split files.exclude management into two tiers to prevent vault patterns from hiding same-named project entries. Dotfiles and blocked paths go to ConfigurationTarget.WorkspaceFolder (vault's .vscode/settings.json), remaining non-autoMount dirs go to ConfigurationTarget.Workspace. The .vscode entry is never managed by the extension. When the vault is not yet a workspace folder, all patterns fall back to workspace scope. Assisted-by: Claude
1 parent 413b9f8 commit 12a418b

7 files changed

Lines changed: 300 additions & 191 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ Style, file layout, patterns: [CONTRIBUTING.md#conventions](CONTRIBUTING.md#conv
3131
## VSCode: Workspace Folder Architecture
3232

3333
- **Single `file://` workspace folder at the vault root** for Quick Open (`Cmd+P`) and `Ctrl+Shift+F` search. VSCode's file discovery and ripgrep indexer only operate on `file://` workspace folders — `obs://` workspace folders provide zero discoverability (confirmed by spike, 2026-05-15).
34-
- **`files.exclude` patterns** hide non-autoMount vault content from Explorer and Quick Open. Patterns are managed via `ConfigurationTarget.Workspace` (routes to `.vscode/settings.json` or `.code-workspace` file automatically). Extension-managed patterns are tracked in `context.workspaceState` and cleaned up on autoMount change or workspace disable.
34+
- **`files.exclude` patterns** hide non-autoMount vault content. Two-tier split: dotfiles + `blocked``ConfigurationTarget.WorkspaceFolder` (`<vault>/.vscode/settings.json`); non-autoMount dirs → `ConfigurationTarget.Workspace`. Tracked in `context.workspaceState`, cleaned up on change or disable.
3535
- **`obs://` FileSystemProvider** remains registered for TreeView sidebar, wikilinks, drag-and-drop, and watch events — it does not back a workspace folder.
3636
- **`FileSearchProvider`/`TextSearchProvider` are proposed (unstable) APIs** as of `@types/vscode@1.118.0`. When stabilized, the extension can switch to a single `obs://` workspace folder with native search, eliminating the `files.exclude` workaround. Check `@types/vscode` for stable availability — do not use while proposed.
3737

packages/vscode/README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,14 +65,14 @@ Non-autoMount vault content (`.obsidian/`, `.trash/`, and any directories not in
6565

6666
**How it works:**
6767

68-
- The extension scans the vault root and adds `files.exclude` patterns for entries not in `autoMount`. Patterns are written to workspace settings (`ConfigurationTarget.Workspace`) and tracked internally for cleanup.
68+
- The extension scans the vault root and adds `files.exclude` patterns for entries not in `autoMount`. Patterns are split into two tiers: vault-global patterns (dotfiles and `blocked` paths from `.obsidian-vfs.json`) are written to `<vault>/.vscode/settings.json`; remaining non-autoMount directories are written to workspace settings. All managed patterns are tracked internally for cleanup.
6969
- When `autoMount` entries change, patterns are re-synced automatically — stale patterns are removed and new ones added.
7070
- When `obsidianVFS.workspace` is disabled, all managed patterns are removed and the workspace folder is deleted.
7171
- The vault's `.git` repository is automatically added to `git.ignoredRepositories` (user-level setting) when the workspace folder is mounted, preventing VS Code's Git extension from listing it in Source Control. The entry is removed when `obsidianVFS.workspace` is disabled.
7272

7373
**Known limitations:**
7474

75-
- **Pattern scope:** `files.exclude` patterns apply to all workspace folders. If a non-autoMount vault directory shares a name with a directory in your project (e.g., both have a `docs/` folder), the project directory will also be hidden. Fix: add the vault directory to `autoMount`, or rename it in your vault.
75+
- **Vault `.vscode/` directory:** The extension creates `.vscode/settings.json` inside the vault for vault-global `files.exclude` patterns (dotfiles and `blocked` paths). These patterns are independent of `autoMount` and apply to any workspace that includes the vault. The `.vscode/` directory itself is never managed by the extension — if you want to hide it, add `.vscode` to your own `files.exclude`. When `obsidianVFS.workspace` is disabled, all managed patterns are removed from both folder and workspace settings. If your vault is git-tracked, consider adding `.vscode/` to the vault's `.gitignore`.
7676
- **Not a security boundary:** `files.exclude` hides content from Explorer and Quick Open but does not enforce access restrictions. The `obs://` FileSystemProvider's path security (`allowed`/`blocked` lists in `.obsidian-vfs.json`) applies to TreeView, wikilink, and drag-and-drop operations.
7777
- **Title bar:** Adding the vault as a workspace folder creates a multi-root workspace. VS Code may show "UNTITLED (WORKSPACE)" in the title bar.
7878

@@ -92,15 +92,15 @@ Enable `obsidianVFS.workspaceFile` to fix this. The extension generates a `<proj
9292
1. The extension creates `<project-name>.code-workspace` in your project root containing the local folder(s) and the `file://` vault folder entry (named `obs://<VaultName>`).
9393
2. A notification asks: _"Open workspace file? This will reload the window."_
9494
3. If you click **Open**, the window reloads once. After that, the title bar shows the project name and the vault is part of the saved workspace.
95-
4. On subsequent activations, the extension detects you're already in a saved workspace and skips the prompt. `files.exclude` patterns are written to the `.code-workspace` file's `settings` section (via `ConfigurationTarget.Workspace`) instead of `.vscode/settings.json`.
96-
5. If you click **Not Now**, the extension falls back to adding the vault dynamically via `updateWorkspaceFolders` (with `files.exclude` patterns in `.vscode/settings.json`). You can open the generated workspace file later.
95+
4. On subsequent activations, the extension detects you're already in a saved workspace and skips the prompt. Vault-global `files.exclude` patterns (dotfiles and `blocked` paths) are written to `<vault>/.vscode/settings.json`; workspace-specific patterns (non-autoMount directories) are written to workspace settings (routed to the `.code-workspace` file when one is active, or to the project's `.vscode/settings.json` otherwise).
96+
5. If you click **Not Now**, the extension falls back to adding the vault dynamically via `updateWorkspaceFolders` (with `files.exclude` patterns in the vault's `.vscode/settings.json`). You can open the generated workspace file later.
9797

9898
**UX trade-offs:**
9999

100100
- **One-time window reload** — opening the workspace file causes VS Code to reload the window. This only happens once; after that the workspace file is saved and reloads are not needed.
101101
- **File on disk** — a `.code-workspace` file is created in your project root. You can commit it to version control (so teammates get the same workspace layout) or add it to `.gitignore`.
102102
- **No overwrite** — if a `.code-workspace` file already exists (e.g., from a previous run or your own), the extension will not overwrite it. It offers to open the existing file instead.
103-
- **`files.exclude` containment**with a `.code-workspace` file, `files.exclude` patterns are scoped to the workspace file rather than `.vscode/settings.json`. This reduces the risk of pattern collisions with project directories (see Known limitations above).
103+
- **`files.exclude` containment**vault-global patterns (dotfiles and `blocked` paths) are scoped to the vault workspace folder, preventing vault dotfiles (`.git`, `.obsidian`) from hiding same-named entries in your project. Workspace-specific patterns (non-autoMount directories like `00-inbox`, `40-log`) use workspace settings and are unlikely to collide with project entries.
104104

105105
**When to use each setting:**
106106

packages/vscode/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "obsidian-vfs",
33
"displayName": "Obsidian VFS",
44
"description": "Browse, search, and edit your Obsidian vault directly in VSCode via a virtual file system (obs://)",
5-
"version": "0.3.1",
5+
"version": "0.3.2",
66
"private": true,
77
"publisher": "otaviof",
88
"engines": {

packages/vscode/src/extension.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -597,7 +597,7 @@ describe("configuration change listener", () => {
597597

598598
expect(mockRemoveWF).toHaveBeenCalledWith("/vault");
599599
expect(mockAddWF).not.toHaveBeenCalled();
600-
expect(mockClearExclude).toHaveBeenCalledWith([]);
600+
expect(mockClearExclude).toHaveBeenCalledWith("/vault", []);
601601
expect(mockIncludeGit).toHaveBeenCalledWith("/vault");
602602
});
603603

@@ -641,7 +641,7 @@ describe("configuration change listener", () => {
641641
await new Promise((r) => setTimeout(r, 0));
642642

643643
expect(mockRemoveWF).toHaveBeenCalledWith("/vault");
644-
expect(mockClearExclude).toHaveBeenCalledWith([]);
644+
expect(mockClearExclude).toHaveBeenCalledWith("/vault", []);
645645
expect(mockAddWF).not.toHaveBeenCalled();
646646
});
647647

packages/vscode/src/extension.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
118118

119119
const runClear = (): void => {
120120
excludeSync = excludeSync.then(async () => {
121-
await clearManagedExcludes(managedExcludes);
121+
await clearManagedExcludes(tracker.context.physicalPath, managedExcludes);
122122
managedExcludes = [];
123123
await context.workspaceState.update(managedExcludesKey, []);
124124
});
@@ -138,13 +138,6 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
138138
} else if (config.workspaceFile && !alreadySaved && config.autoMount.length > 0) {
139139
try {
140140
await excludeVaultFromGitDetection(tracker.context.physicalPath);
141-
managedExcludes = await syncFilesExclude(
142-
tracker.context.physicalPath,
143-
config.autoMount,
144-
blocked,
145-
managedExcludes,
146-
);
147-
await context.workspaceState.update(managedExcludesKey, managedExcludes);
148141

149142
const wfResult = generateWorkspaceFile(tracker.context.physicalPath, tracker.context.name);
150143
outputChannel.appendLine(`Workspace file: ${wfResult.status}${wfResult.fileUri.fsPath}`);
@@ -161,6 +154,13 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
161154
const addResult = addVaultWorkspaceFolder(tracker.context.physicalPath, tracker.context.name);
162155
const detail = "reason" in addResult ? ` — ${addResult.reason}` : "";
163156
outputChannel.appendLine(`Workspace folder: ${addResult.status}${detail}`);
157+
managedExcludes = await syncFilesExclude(
158+
tracker.context.physicalPath,
159+
config.autoMount,
160+
blocked,
161+
managedExcludes,
162+
);
163+
await context.workspaceState.update(managedExcludesKey, managedExcludes);
164164
} catch (err) {
165165
const msg = err instanceof Error ? err.message : String(err);
166166
outputChannel.appendLine(`Workspace file: skipped — ${msg}`);

0 commit comments

Comments
 (0)