Skip to content

Commit 056f5a7

Browse files
committed
Default notes directory
1 parent 2fbf18a commit 056f5a7

8 files changed

Lines changed: 287 additions & 9 deletions

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ All notable changes to AS Notes will be documented here.
66

77
## [2.2.3] - 2026-03-19
88

9-
- Templates
9+
- Templates and template placeholders
10+
- Modified default note placement behaviour
1011
- Kanban boards markdown formatting
1112
- Kanban board card conversion from task (markdown todo)
1213

README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,10 @@ All table operations in the slash command menu (`/`) are Pro features. Free user
264264

265265
See [Slash Commands](#slash-commands) for the full list of table commands.
266266

267+
### Create Note
268+
269+
Run **AS Notes: Create Note** from the Command Palette to create a new note. You will be prompted for a title and the file is created in the configured notes folder (default: `notes/`).
270+
267271
### Encrypted notes
268272

269273
Pro users can store sensitive notes in encrypted files. Any file with the `.enc.md` extension is treated as an encrypted note - it is excluded from the search index and never read as plain text by the extension.
@@ -283,7 +287,7 @@ Pro users can store sensitive notes in encrypted files. Any file with the `.enc.
283287
**Commands:**
284288
- `AS Notes: Set Encryption Key` - save passphrase to OS keychain
285289
- `AS Notes: Clear Encryption Key` - remove the stored passphrase
286-
- `AS Notes: Create Encrypted Note` - create a new named `.enc.md` file
290+
- `AS Notes: Create Encrypted Note` - create a new named `.enc.md` file in the notes folder
287291
- `AS Notes: Create Encrypted Journal Note` - create today's journal entry as `.enc.md`
288292
- `AS Notes: Encrypt All Notes` - encrypt all plaintext `.enc.md` files
289293
- `AS Notes: Decrypt All Notes` - decrypt all encrypted `.enc.md` files
@@ -374,7 +378,9 @@ Nesting works to arbitrary depth. The extension always identifies the innermost
374378

375379
### Auto-create missing pages
376380

377-
Navigating to a page that doesn't exist creates it automatically, so you can write forward-references before the target page exists.
381+
Navigating to a page that doesn't exist creates it automatically in the configured notes folder (default: `notes/`). You can write forward-references before the target page exists.
382+
383+
When `as-notes.createNotesInCurrentDirectory` is enabled, new pages are created in the current editing file's directory instead, unless the source file is in the journal folder (in which case the notes folder is always used).
378384

379385
### Hover tooltips
380386

@@ -656,6 +662,8 @@ Front-matter holds the structured fields; the Markdown body is the card descript
656662
|---|---|---|
657663
| `as-notes.periodicScanInterval` | `300` | Seconds between automatic background scans for file changes. Set to `0` to disable. Minimum: `30`. |
658664
| `as-notes.journalFolder` | `journals` | Folder for daily journal files, relative to workspace root. |
665+
| `as-notes.notesFolder` | `notes` | Folder for new notes, relative to workspace root. Used when creating pages via wikilink navigation and the Create Note / Create Encrypted Note commands. |
666+
| `as-notes.createNotesInCurrentDirectory` | `false` | When enabled, new notes created via wikilink navigation are placed in the current editing file's directory instead of the notes folder. Ignored when the source file is in the journal folder. |
659667
| `as-notes.templateFolder` | `templates` | Folder for note templates, relative to workspace root. Templates are markdown files inserted via the `/Template` slash command. |
660668
| `as-notes.licenceKey` | *(empty)* | AS Notes Pro licence key (format: `ASNO-XXXX-XXXX-XXXX-XXXX`). Enter via **AS Notes: Enter Licence Key** in the Command Palette or directly in Settings. Scope: machine (not synced). |
661669
| `as-notes.enableLogging` | `false` | Enable diagnostic logging to `.asnotes/logs/`. Rolling 10 MB files, max 5. Requires reload after changing. Also activated by setting the `AS_NOTES_DEBUG=1` environment variable. |

TECHNICAL.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1235,7 +1235,15 @@ The command URI carries:
12351235
{directory of source file}/{pageFileName}.md
12361236
```
12371237

1238-
All target files live in the same directory as the file containing the link. There is no support for cross-directory linking — this is intentional to keep the mental model simple (one folder = one wiki).
1238+
This is used by the rename tracker where the file already exists in a known location.
1239+
1240+
`WikilinkFileService.resolveNewFileTargetUri()` determines where **new** files should be created, respecting user settings:
1241+
1242+
- By default, new files are created in the configured `notesFolder` (default: `notes/`).
1243+
- When `createNotesInCurrentDirectory` is enabled, new files are placed in the source file's directory -- unless the source file is inside the journal folder, in which case the notes folder is always used.
1244+
- Used by `WikilinkDocumentLinkProvider`, the `navigateToPage` context menu command, and the `navigateWikilink` command.
1245+
1246+
All target files can live anywhere in the workspace. The index handles global resolution.
12391247

12401248
### Case-insensitive matching
12411249

@@ -1858,7 +1866,7 @@ The `as-notes.navigateToPage` command (registered in `extension.ts`) enables rig
18581866

18591867
1. Extracts all wikilinks from the current line
18601868
2. Finds the innermost wikilink at the cursor position via `WikilinkService.findInnermostWikilinkAtOffset()`
1861-
3. Resolves the target URI via `WikilinkFileService.resolveTargetUri()`
1869+
3. Resolves the target URI via `WikilinkFileService.resolveNewFileTargetUri()` (respects `notesFolder` and `createNotesInCurrentDirectory` settings)
18621870
4. Calls `WikilinkFileService.navigateToFile()` — which uses index-aware resolution (global filename match, alias support, case-insensitive fallback) and auto-creates the file if it doesn't exist
18631871

18641872
This provides the same navigation as Ctrl+click (DocumentLink) but via an explicit context menu entry. The command appears in the `editor/context` menu alongside "View Backlinks" when `as-notes.fullMode` is active and the editor language is markdown.
@@ -2647,6 +2655,10 @@ Tests use vitest and are split across fourteen test files (key files described b
26472655

26482656
1. **Path distance** (10 tests) — same directory (0), nested subdirectory (1), sibling directories (2), root to deep (3), deep to root (3), divergent paths, case-insensitive comparison, deeply nested to root, same prefix different branch, single segment root.
26492657

2658+
### `WikilinkFileService.resolveNewFileTargetUri.test.ts` (9 tests)
2659+
2660+
1. **New file target resolution** (9 tests) — default notesFolder, custom notesFolder, createNotesInCurrentDirectory with non-journal source, journal folder override, nested journal subfolder override, empty notesFolder (workspace root), leading/trailing slash normalisation, explicit false setting, resolveTargetUri unchanged.
2661+
26502662
### `WikilinkCompletionProvider.test.ts` (30 tests)
26512663

26522664
1. **Bracket detection** (11 tests) — no brackets, simple `[[`, after text, with text typed, already closed, nested innermost detection, inner closed leaving outer open, all brackets closed, multiple unclosed, partially closed, single `[`.

vs-code-extension/package.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@
102102
"command": "as-notes.decryptNotes",
103103
"title": "AS Notes: Decrypt All Notes"
104104
},
105+
{
106+
"command": "as-notes.createNote",
107+
"title": "AS Notes: Create Note"
108+
},
105109
{
106110
"command": "as-notes.createEncryptedFile",
107111
"title": "AS Notes: Create Encrypted Note"
@@ -276,6 +280,16 @@
276280
"type": "string",
277281
"default": "templates",
278282
"description": "Folder for note templates, relative to workspace root. Templates are markdown files that can be inserted via the /Template slash command."
283+
},
284+
"as-notes.notesFolder": {
285+
"type": "string",
286+
"default": "notes",
287+
"description": "Folder for new notes, relative to workspace root. Used when creating pages via wikilink navigation and the Create Note / Create Encrypted Note commands."
288+
},
289+
"as-notes.createNotesInCurrentDirectory": {
290+
"type": "boolean",
291+
"default": false,
292+
"description": "When enabled, new notes created via wikilink navigation are placed in the current editing file's directory instead of the notes folder. Ignored when the source file is in the journal folder."
279293
}
280294
}
281295
},

vs-code-extension/src/WikilinkDocumentLinkProvider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export class WikilinkDocumentLinkProvider implements vscode.DocumentLinkProvider
5353
);
5454

5555
const wl = segment.wikilink;
56-
const targetUri = this.fileService.resolveTargetUri(document.uri, wl.pageFileName);
56+
const targetUri = this.fileService.resolveNewFileTargetUri(document.uri, wl.pageFileName);
5757

5858
const commandUri = vscode.Uri.parse(
5959
`command:as-notes.navigateWikilink?${encodeURIComponent(JSON.stringify({

vs-code-extension/src/WikilinkFileService.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,56 @@ export class WikilinkFileService {
4343
return vscode.Uri.file(targetPath);
4444
}
4545

46+
/**
47+
* Build the URI for a **new** wikilink target file, respecting the
48+
* `notesFolder` and `createNotesInCurrentDirectory` settings.
49+
*
50+
* - If `createNotesInCurrentDirectory` is true AND the source file is
51+
* NOT inside the journal folder: uses the source file's directory.
52+
* - Otherwise: uses the configured `notesFolder` (relative to workspace root).
53+
*
54+
* @param sourceUri - URI of the document containing the wikilink
55+
* @param pageFileName - Sanitised page filename (without extension)
56+
* @returns URI pointing to `{targetDir}/{pageFileName}.md`
57+
*/
58+
resolveNewFileTargetUri(sourceUri: vscode.Uri, pageFileName: string): vscode.Uri {
59+
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri;
60+
if (!workspaceRoot) {
61+
// No workspace — fall back to source directory
62+
return this.resolveTargetUri(sourceUri, pageFileName);
63+
}
64+
65+
const config = vscode.workspace.getConfiguration('as-notes');
66+
const createInCurrentDir = config.get<boolean>('createNotesInCurrentDirectory', false);
67+
68+
if (createInCurrentDir && !this.isInsideJournalFolder(sourceUri, workspaceRoot)) {
69+
return this.resolveTargetUri(sourceUri, pageFileName);
70+
}
71+
72+
const notesFolder = config.get<string>('notesFolder', 'notes');
73+
const normalised = notesFolder.trim().replace(/^[/\\]+|[/\\]+$/g, '');
74+
const targetDir = normalised
75+
? path.join(workspaceRoot.fsPath, normalised)
76+
: workspaceRoot.fsPath;
77+
const targetPath = path.join(targetDir, `${pageFileName}.md`);
78+
return vscode.Uri.file(targetPath);
79+
}
80+
81+
/**
82+
* Check whether a source URI is inside the configured journal folder.
83+
*/
84+
private isInsideJournalFolder(sourceUri: vscode.Uri, workspaceRoot: vscode.Uri): boolean {
85+
const config = vscode.workspace.getConfiguration('as-notes');
86+
const journalFolder = config.get<string>('journalFolder', 'journals');
87+
const normalised = journalFolder.trim().replace(/^[/\\]+|[/\\]+$/g, '');
88+
if (!normalised) {
89+
return false;
90+
}
91+
const journalDir = path.join(workspaceRoot.fsPath, normalised).replace(/\\/g, '/').toLowerCase();
92+
const sourceDir = path.dirname(sourceUri.fsPath).replace(/\\/g, '/').toLowerCase();
93+
return sourceDir === journalDir || sourceDir.startsWith(journalDir + '/');
94+
}
95+
4696
/**
4797
* Resolve a wikilink target using the persistent index for global resolution.
4898
*

vs-code-extension/src/extension.ts

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,13 @@ async function enterFullMode(
706706
fs.mkdirSync(journalFolderPath, { recursive: true });
707707
}
708708

709+
const notesFolder = config.get<string>('notesFolder', 'notes');
710+
const normalisedNotes = notesFolder.trim().replace(/^[/\\]+|[/\\]+$/g, '');
711+
if (normalisedNotes) {
712+
const notesFolderPath = path.join(workspaceRoot.fsPath, normalisedNotes);
713+
fs.mkdirSync(notesFolderPath, { recursive: true });
714+
}
715+
709716
ignoreService = new IgnoreService(path.join(workspaceRoot.fsPath, IGNORE_FILE));
710717
indexScanner = new IndexScanner(indexService, workspaceRoot, ignoreService, logService);
711718

@@ -1369,7 +1376,7 @@ async function enterFullMode(
13691376

13701377
if (!wikilink) { return; } // Not on a wikilink — do nothing
13711378

1372-
const targetUri = fileService.resolveTargetUri(editor.document.uri, wikilink.pageFileName);
1379+
const targetUri = fileService.resolveNewFileTargetUri(editor.document.uri, wikilink.pageFileName);
13731380
await fileService.navigateToFile(targetUri, wikilink.pageFileName, editor.document.uri);
13741381
}),
13751382
);
@@ -1587,6 +1594,34 @@ async function enterFullMode(
15871594
}),
15881595
);
15891596

1597+
fullModeDisposables.push(
1598+
vscode.commands.registerCommand('as-notes.createNote', async () => {
1599+
const title = await vscode.window.showInputBox({
1600+
prompt: 'Note title',
1601+
placeHolder: 'My note',
1602+
ignoreFocusOut: true,
1603+
});
1604+
if (!title) { return; }
1605+
const config = vscode.workspace.getConfiguration('as-notes');
1606+
const notesFolder = config.get<string>('notesFolder', 'notes');
1607+
const normalised = notesFolder.trim().replace(/^[/\\]+|[/\\]+$/g, '');
1608+
const folderUri = normalised
1609+
? vscode.Uri.joinPath(workspaceRoot, normalised)
1610+
: workspaceRoot;
1611+
await vscode.workspace.fs.createDirectory(folderUri);
1612+
const filename = `${sanitiseFileName(title)}.md`;
1613+
const fileUri = vscode.Uri.joinPath(folderUri, filename);
1614+
try {
1615+
await vscode.workspace.fs.stat(fileUri);
1616+
// File already exists — just open it
1617+
} catch {
1618+
await vscode.workspace.fs.writeFile(fileUri, Buffer.from('', 'utf-8'));
1619+
}
1620+
const doc = await vscode.workspace.openTextDocument(fileUri);
1621+
await vscode.window.showTextDocument(doc);
1622+
}),
1623+
);
1624+
15901625
fullModeDisposables.push(
15911626
vscode.commands.registerCommand('as-notes.createEncryptedFile', async () => {
15921627
if (!hasProEditor()) {
@@ -1599,8 +1634,15 @@ async function enterFullMode(
15991634
ignoreFocusOut: true,
16001635
});
16011636
if (!title) { return; }
1637+
const config = vscode.workspace.getConfiguration('as-notes');
1638+
const notesFolder = config.get<string>('notesFolder', 'notes');
1639+
const normalised = notesFolder.trim().replace(/^[/\\]+|[/\\]+$/g, '');
1640+
const folderUri = normalised
1641+
? vscode.Uri.joinPath(workspaceRoot, normalised)
1642+
: workspaceRoot;
1643+
await vscode.workspace.fs.createDirectory(folderUri);
16021644
const filename = `${sanitiseFileName(title)}.enc.md`;
1603-
const fileUri = vscode.Uri.joinPath(workspaceRoot, filename);
1645+
const fileUri = vscode.Uri.joinPath(folderUri, filename);
16041646
try {
16051647
await vscode.workspace.fs.stat(fileUri);
16061648
// File already exists — just open it
@@ -1750,7 +1792,11 @@ async function enterFullMode(
17501792
}) => {
17511793
const targetUri = vscode.Uri.parse(args.targetUri);
17521794
const sourceUri = vscode.Uri.parse(args.sourceUri);
1753-
await fileService.navigateToFile(targetUri, args.pageFileName, sourceUri);
1795+
// Re-resolve the creation target using current settings so that the
1796+
// correct notes folder is used even if the link provider cached a
1797+
// stale URI (e.g. when the document was first opened).
1798+
const creationUri = fileService.resolveNewFileTargetUri(sourceUri, args.pageFileName);
1799+
await fileService.navigateToFile(creationUri, args.pageFileName, sourceUri);
17541800
}),
17551801
);
17561802

@@ -2127,6 +2173,14 @@ async function initWorkspace(context: vscode.ExtensionContext): Promise<void> {
21272173
fs.writeFileSync(journalTemplatePath, DEFAULT_JOURNAL_TEMPLATE, 'utf-8');
21282174
}
21292175

2176+
// Create notes/ directory for new notes
2177+
const notesFolder = templateConfig.get<string>('notesFolder', 'notes');
2178+
const normalisedNotes = notesFolder.trim().replace(/^[/\\]+|[/\\]+$/g, '');
2179+
if (normalisedNotes) {
2180+
const notesFolderPath = path.join(workspaceRoot.fsPath, normalisedNotes);
2181+
fs.mkdirSync(notesFolderPath, { recursive: true });
2182+
}
2183+
21302184
// Create .gitignore inside .asnotes/ to exclude the DB file
21312185
fs.writeFileSync(path.join(asnotesDir, '.gitignore'), 'index.db\n');
21322186

0 commit comments

Comments
 (0)