Skip to content

Commit f53330f

Browse files
committed
Extended folder exclusions for index
1 parent 2efda26 commit f53330f

8 files changed

Lines changed: 440 additions & 421 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,7 @@ Update `version` in `package.json` and add an entry to `CHANGELOG.md`.
532532
**Step 2 - publish to the VS Code Marketplace**
533533

534534
```bash
535+
cd .\vs-code-extension\
535536
npm run build
536537
npx @vscode/vsce package
537538
npx @vscode/vsce login appsoftwareltd # enter PAT token if auth expired
@@ -541,6 +542,7 @@ npx @vscode/vsce publish
541542
**Step 3 - tag and push**
542543

543544
```bash
545+
cd ..
544546
git add .
545547
git commit -m "Release v2.3.1" # change version
546548
git tag v2.3.1 # change version

TECHNICAL.md

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,14 @@ reload(): void
257257

258258
Patterns follow `.gitignore` syntax, parsed by the [`ignore`](https://www.npmjs.com/package/ignore) npm package — the standard implementation used by many tools. `IgnoreService` normalises backslash paths to forward slashes on Windows before passing them to `ignore`.
259259

260+
In addition to user-owned `.asnotesignore` content, the extension builds a runtime ignore layer via `createConfiguredIgnoreService()` for mandatory exclusions derived from current configuration. These patterns are added after `.asnotesignore` is loaded so they remain non-optional even if a user adds negation rules.
261+
262+
Mandatory runtime exclusions are:
263+
264+
- `.asnotes/`
265+
- configured `templateFolder`
266+
- configured `assetPath`
267+
260268
**`.asnotesignore` lifecycle:**
261269

262270
- Created by `initWorkspace()` at the workspace root (same level as `.asnotes/`) if it does not already exist.
@@ -272,7 +280,7 @@ The create-if-missing logic is centralised in a private `ensureIgnoreFile(worksp
272280
| `rebuildIndex()` | At the start of every manual rebuild |
273281
| `startPeriodicScan()` setInterval callback | On every periodic scan tick |
274282

275-
After `ensureIgnoreFile()` is called in `rebuildIndex()` and `startPeriodicScan()`, `ignoreService?.reload()` is called immediately so the in-memory patterns reflect any changes (including recreation after deletion) before the scan proceeds.
283+
After `ensureIgnoreFile()` is called in `rebuildIndex()` and `startPeriodicScan()`, `ignoreService?.reload()` is called immediately so the in-memory patterns reflect any changes (including recreation after deletion) before the scan proceeds. When `templateFolder` or `assetPath` changes, the extension recreates the configured ignore service and re-runs a stale scan so the mandatory runtime exclusions update immediately without rewriting `.asnotesignore`.
276284

277285
**Integration with IndexScanner:**
278286

@@ -2637,7 +2645,17 @@ In `extension.ts`, all index update triggers (`onDidSaveTextDocument`, `onDidCha
26372645

26382646
A private `isEncryptedFileUri(uri)` helper centralises the `.enc.md` check within `extension.ts`.
26392647

2640-
**2. `.asnotesignore` patterns (user exclusions)**
2648+
**2. Runtime ignore exclusions**
2649+
2650+
The index also excludes mandatory runtime directories built from extension configuration:
2651+
2652+
- `.asnotes/`
2653+
- configured `templateFolder`
2654+
- configured `assetPath`
2655+
2656+
These are enforced through `IgnoreService` rather than written into `.asnotesignore`, so settings changes take effect immediately and user ignore files remain user-owned.
2657+
2658+
**3. `.asnotesignore` patterns (user exclusions)**
26412659

26422660
See [IgnoreService and .asnotesignore](#ignoreservice-and-asnotesignore) in the Persistent index section.
26432661

@@ -2664,19 +2682,19 @@ AS Notes delegates file drop and paste to VS Code's **built-in** markdown editor
26642682

26652683
### Workspace configuration
26662684

2667-
`applyAssetPathSettings()` in `src/ImageDropProvider.ts` reads `as-notes.assetPath` (default `assets/images`) and writes:
2685+
`applyAssetPathSettings()` in `src/ImageDropProvider.ts` reads `as-notes.assetPath` (default `assets`) and writes:
26682686

26692687
```json
26702688
"markdown.copyFiles.destination": {
2671-
"**/*.md": "assets/images/${fileName}"
2689+
"**/*.md": "assets/${fileName}"
26722690
}
26732691
```
26742692

26752693
to `.vscode/settings.json` at workspace scope. `${fileName}` is a built-in VS Code variable that resolves to the original filename of the dropped/pasted file.
26762694

26772695
| Setting | Default | Description |
26782696
|---|---|---|
2679-
| `as-notes.assetPath` | `assets/images` | Workspace-relative folder where dropped/pasted files are saved |
2697+
| `as-notes.assetPath` | `assets` | Workspace-relative folder where dropped/pasted files are saved |
26802698

26812699
**Trigger points** (all in `extension.ts`):
26822700

vs-code-extension/README.md

Lines changed: 255 additions & 397 deletions
Large diffs are not rendered by default.

vs-code-extension/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@
292292
},
293293
"as-notes.assetPath": {
294294
"type": "string",
295-
"default": "assets/images",
295+
"default": "assets",
296296
"description": "Folder where dropped or pasted files are saved by VS Code's built-in markdown editor, relative to the AS Notes root directory. AS Notes configures the built-in markdown.copyFiles.destination setting to use this path."
297297
},
298298
"as-notes.createNotesInCurrentDirectory": {

vs-code-extension/src/IgnoreService.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,54 @@ import * as fs from 'fs';
22
import ignore, { type Ignore } from 'ignore';
33
import { formatLogError, getActiveLogger } from './LogService.js';
44

5+
export interface IgnoreServiceOptions {
6+
/**
7+
* Additional ignore patterns layered after `.asnotesignore` so they
8+
* remain mandatory and cannot be un-ignored by user config.
9+
*/
10+
readonly additionalPatterns?: readonly string[];
11+
}
12+
13+
export interface RuntimeIgnoreConfig {
14+
readonly templateFolder?: string;
15+
readonly assetPath?: string;
16+
}
17+
18+
function normaliseRelativeDirectoryPath(dir?: string): string {
19+
return (dir ?? '').trim().replace(/^[/\\]+|[/\\]+$/g, '').replace(/\\/g, '/');
20+
}
21+
22+
function toDirectoryPattern(dir?: string): string | undefined {
23+
const normalised = normaliseRelativeDirectoryPath(dir);
24+
return normalised ? `${normalised}/` : undefined;
25+
}
26+
27+
/**
28+
* Runtime exclusions that are part of AS Notes configuration/state rather
29+
* than user-owned `.asnotesignore` content.
30+
*/
31+
export function buildMandatoryIgnorePatterns(config: RuntimeIgnoreConfig = {}): string[] {
32+
const patterns = ['.asnotes/'];
33+
const templatePattern = toDirectoryPattern(config.templateFolder);
34+
if (templatePattern) {
35+
patterns.push(templatePattern);
36+
}
37+
const assetPattern = toDirectoryPattern(config.assetPath);
38+
if (assetPattern) {
39+
patterns.push(assetPattern);
40+
}
41+
return Array.from(new Set(patterns));
42+
}
43+
44+
export function createConfiguredIgnoreService(
45+
ignoreFilePath: string,
46+
config: RuntimeIgnoreConfig = {},
47+
): IgnoreService {
48+
return new IgnoreService(ignoreFilePath, {
49+
additionalPatterns: buildMandatoryIgnorePatterns(config),
50+
});
51+
}
52+
553
/**
654
* Reads and parses an `.asnotesignore` file (`.gitignore` syntax) and exposes
755
* an `isIgnored()` check for use during index scanning.
@@ -10,11 +58,16 @@ import { formatLogError, getActiveLogger } from './LogService.js';
1058
*/
1159
export class IgnoreService {
1260
private ig: Ignore;
61+
private readonly additionalPatterns: readonly string[];
1362

1463
/**
1564
* @param ignoreFilePath Absolute path to the `.asnotesignore` file.
1665
*/
17-
constructor(private readonly ignoreFilePath: string) {
66+
constructor(
67+
private readonly ignoreFilePath: string,
68+
options: IgnoreServiceOptions = {},
69+
) {
70+
this.additionalPatterns = options.additionalPatterns ?? [];
1871
this.ig = this.load();
1972
}
2073

@@ -43,6 +96,9 @@ export class IgnoreService {
4396
private load(): Ignore {
4497
const instance = ignore();
4598
if (!fs.existsSync(this.ignoreFilePath)) {
99+
if (this.additionalPatterns.length > 0) {
100+
instance.add(this.additionalPatterns);
101+
}
46102
return instance;
47103
}
48104
try {
@@ -51,6 +107,9 @@ export class IgnoreService {
51107
} catch (err) {
52108
getActiveLogger().warn('IgnoreService', `could not read ${this.ignoreFilePath}: ${formatLogError(err)}`);
53109
}
110+
if (this.additionalPatterns.length > 0) {
111+
instance.add(this.additionalPatterns);
112+
}
54113
return instance;
55114
}
56115
}

vs-code-extension/src/ImageDropProvider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import * as vscode from 'vscode';
2222
*/
2323
export async function applyAssetPathSettings(): Promise<void> {
2424
const config = vscode.workspace.getConfiguration('as-notes');
25-
const assetPath = config.get<string>('assetPath', 'assets/images');
25+
const assetPath = config.get<string>('assetPath', 'assets');
2626
const rootDirectory = config.get<string>('rootDirectory', '').trim().replace(/^[/\\]+|[/\\]+$/g, '');
2727

2828
const target = vscode.ConfigurationTarget.Workspace;

vs-code-extension/src/extension.ts

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import { ensurePreCommitHook } from './GitHookService.js';
4040
import { applyAssetPathSettings } from './ImageDropProvider.js';
4141
import { LogService, NO_OP_LOGGER, formatLogError, setActiveLogger } from './LogService.js';
4242
import { findInnermostOpenBracket, hasNewCompleteWikilink } from './CompletionUtils.js';
43-
import { IgnoreService } from './IgnoreService.js';
43+
import { IgnoreService, createConfiguredIgnoreService } from './IgnoreService.js';
4444
import { SlashCommandProvider } from './SlashCommandProvider.js';
4545
import { openDatePicker } from './DatePickerService.js';
4646
import { insertTaskDueDate, insertTaskCompletionDate, insertTagAtTaskStart } from './TaskHashtagService.js';
@@ -774,8 +774,7 @@ async function enterFullMode(
774774
fs.mkdirSync(notesFolderPath, { recursive: true });
775775
}
776776

777-
ignoreService = new IgnoreService(nrp.ignoreFilePath);
778-
indexScanner = new IndexScanner(indexService, nrUri, ignoreService, logService);
777+
refreshIgnoreInfrastructure();
779778

780779
// Shared services — WikilinkService is index-independent, so create early
781780
const wikilinkService = new WikilinkService();
@@ -2216,17 +2215,7 @@ async function enterFullMode(
22162215
);
22172216
const onIgnoreFileChange = (): void => {
22182217
ignoreService?.reload();
2219-
indexScanner?.staleScan().then((summary) => {
2220-
if (summary.newFiles > 0 || summary.staleFiles > 0 || summary.deletedFiles > 0) {
2221-
indexService?.saveToFile();
2222-
completionProvider?.refresh();
2223-
taskPanelProvider?.refresh();
2224-
searchPanelProvider?.refresh();
2225-
backlinkPanelProvider?.refresh();
2226-
calendarPanelProvider?.refresh();
2227-
updateFullModeStatusBar();
2228-
}
2229-
}).catch(err => logService.warn('extension', `stale scan after .asnotesignore change failed: ${formatLogError(err)}`));
2218+
runStaleScanAfterExclusionRefresh('.asnotesignore change');
22302219
};
22312220
ignoreFileWatcher.onDidChange(onIgnoreFileChange);
22322221
ignoreFileWatcher.onDidCreate(onIgnoreFileChange);
@@ -2245,6 +2234,10 @@ async function enterFullMode(
22452234
logService.warn('extension', `failed to apply asset path settings on config change: ${formatLogError(err)}`),
22462235
);
22472236
}
2237+
if (e.affectsConfiguration('as-notes.assetPath') || e.affectsConfiguration('as-notes.templateFolder')) {
2238+
refreshIgnoreInfrastructure();
2239+
runStaleScanAfterExclusionRefresh('runtime exclusion config change');
2240+
}
22482241
if (e.affectsConfiguration('as-notes.rootDirectory')) {
22492242
vscode.window.showWarningMessage(
22502243
'AS Notes: The root directory setting has changed. Reload the window to apply the new root.',
@@ -2477,7 +2470,7 @@ async function initWorkspace(context: vscode.ExtensionContext): Promise<void> {
24772470
indexService = new IndexService(nrp.databasePath, logService);
24782471
await indexService.initDatabase();
24792472

2480-
const initIgnoreService = new IgnoreService(nrp.ignoreFilePath);
2473+
const initIgnoreService = createRuntimeIgnoreService(nrp.ignoreFilePath);
24812474
indexScanner = new IndexScanner(indexService, nrUri, initIgnoreService, logService);
24822475

24832476
const result = await indexScanner.fullScan(progress);
@@ -2942,6 +2935,37 @@ function ensureIgnoreFile(notesRootFsPath: string): void {
29422935
}
29432936
}
29442937

2938+
function createRuntimeIgnoreService(ignoreFilePath: string): IgnoreService {
2939+
const config = vscode.workspace.getConfiguration('as-notes');
2940+
return createConfiguredIgnoreService(ignoreFilePath, {
2941+
templateFolder: config.get<string>('templateFolder', 'templates'),
2942+
assetPath: config.get<string>('assetPath', 'assets'),
2943+
});
2944+
}
2945+
2946+
function refreshIgnoreInfrastructure(): void {
2947+
if (!notesRootPaths) { return; }
2948+
ignoreService = createRuntimeIgnoreService(notesRootPaths.ignoreFilePath);
2949+
if (indexService?.isOpen && notesRootUri) {
2950+
indexScanner = new IndexScanner(indexService, notesRootUri, ignoreService, logService);
2951+
}
2952+
}
2953+
2954+
function runStaleScanAfterExclusionRefresh(reason: string): void {
2955+
if (!indexScanner) { return; }
2956+
indexScanner.staleScan().then((summary) => {
2957+
if (summary.newFiles > 0 || summary.staleFiles > 0 || summary.deletedFiles > 0) {
2958+
indexService?.saveToFile();
2959+
completionProvider?.refresh();
2960+
taskPanelProvider?.refresh();
2961+
searchPanelProvider?.refresh();
2962+
backlinkPanelProvider?.refresh();
2963+
calendarPanelProvider?.refresh();
2964+
updateFullModeStatusBar();
2965+
}
2966+
}).catch(err => logService.warn('extension', `stale scan after ${reason} failed: ${formatLogError(err)}`));
2967+
}
2968+
29452969
function getWorkspaceRoot(): vscode.Uri | undefined {
29462970
return vscode.workspace.workspaceFolders?.[0]?.uri;
29472971
}

vs-code-extension/src/test/IgnoreService.test.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
22
import * as fs from 'fs';
33
import * as os from 'os';
44
import * as path from 'path';
5-
import { IgnoreService } from '../IgnoreService.js';
5+
import { IgnoreService, createConfiguredIgnoreService } from '../IgnoreService.js';
66

77
// ── Helpers ────────────────────────────────────────────────────────────────
88

@@ -212,3 +212,61 @@ describe('IgnoreService — default patterns', () => {
212212
expect(svc.isIgnored('journal/2024-01-01.md')).toBe(false);
213213
});
214214
});
215+
216+
describe('IgnoreService — mandatory runtime exclusions', () => {
217+
it('always excludes .asnotes, templateFolder, and assetPath directories', () => {
218+
const svc = createConfiguredIgnoreService(ignoreFilePath, {
219+
templateFolder: 'templates',
220+
assetPath: 'assets',
221+
});
222+
223+
expect(svc.isIgnored('.asnotes/index.db')).toBe(true);
224+
expect(svc.isIgnored('.asnotes/logs/main.log')).toBe(true);
225+
expect(svc.isIgnored('templates/Journal.md')).toBe(true);
226+
expect(svc.isIgnored('assets/diagram.md')).toBe(true);
227+
expect(svc.isIgnored('assets/images/diagram.md')).toBe(true);
228+
expect(svc.isIgnored('notes/page.md')).toBe(false);
229+
});
230+
231+
it('normalises configured directory paths and preserves mandatory exclusions across reload', () => {
232+
writeIgnore('archive/\n');
233+
const svc = createConfiguredIgnoreService(ignoreFilePath, {
234+
templateFolder: '/templates/',
235+
assetPath: '\\assets\\images\\',
236+
});
237+
238+
expect(svc.isIgnored('templates/Journal.md')).toBe(true);
239+
expect(svc.isIgnored('assets/images/diagram.md')).toBe(true);
240+
expect(svc.isIgnored('.asnotes/index.db')).toBe(true);
241+
expect(svc.isIgnored('archive/page.md')).toBe(true);
242+
243+
writeIgnore('private/\n');
244+
svc.reload();
245+
246+
expect(svc.isIgnored('.asnotes/index.db')).toBe(true);
247+
expect(svc.isIgnored('templates/Journal.md')).toBe(true);
248+
expect(svc.isIgnored('assets/images/diagram.md')).toBe(true);
249+
expect(svc.isIgnored('archive/page.md')).toBe(false);
250+
expect(svc.isIgnored('private/page.md')).toBe(true);
251+
});
252+
253+
it('reflects settings changes when a new configured service is created', () => {
254+
const initialSvc = createConfiguredIgnoreService(ignoreFilePath, {
255+
templateFolder: 'templates',
256+
assetPath: 'assets',
257+
});
258+
expect(initialSvc.isIgnored('templates/Journal.md')).toBe(true);
259+
expect(initialSvc.isIgnored('static/diagram.md')).toBe(false);
260+
expect(initialSvc.isIgnored('assets/diagram.md')).toBe(true);
261+
262+
const updatedSvc = createConfiguredIgnoreService(ignoreFilePath, {
263+
templateFolder: 'snippets',
264+
assetPath: 'static',
265+
});
266+
expect(updatedSvc.isIgnored('.asnotes/index.db')).toBe(true);
267+
expect(updatedSvc.isIgnored('templates/Journal.md')).toBe(false);
268+
expect(updatedSvc.isIgnored('snippets/Journal.md')).toBe(true);
269+
expect(updatedSvc.isIgnored('assets/diagram.md')).toBe(false);
270+
expect(updatedSvc.isIgnored('static/diagram.md')).toBe(true);
271+
});
272+
});

0 commit comments

Comments
 (0)