From 14cfd91ac2b060aab12dea6019aec5bf803f93ff Mon Sep 17 00:00:00 2001 From: Ethan Graf Date: Tue, 28 Jul 2026 22:05:42 -0400 Subject: [PATCH] Seed a wikilink-created note with its title as an H1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A wikilink names the note the author meant to write, so that name is real information — seeding it as an H1 saves retyping what you just typed and gives useDocumentTitle a heading to read. `createFile` gains optional initial content, threaded through the synced provider so the remote doc carries it rather than being created empty and filled by a later sync. The `.md` extension is applied explicitly rather than relying on createFile's auto-append, which only fires when the name has no dot at all and would otherwise land `[[Release v1.2]]` as an extensionless file. Scoped to wikilinks on purpose: "New File" names a note `Untitled`, and an `# Untitled` heading would just be noise to delete. Co-Authored-By: Claude Opus 5 --- .../providers/localFilesystemProvider.ts | 8 +++++-- .../syncedVaultFilesystemProvider.ts | 10 ++++++-- src/filesystem/types.ts | 7 +++++- src/wikilinks/WikiLinkVaultContext.tsx | 5 +++- src/wikilinks/noteTemplate.test.ts | 23 +++++++++++++++++++ src/wikilinks/noteTemplate.ts | 17 ++++++++++++++ 6 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 src/wikilinks/noteTemplate.test.ts create mode 100644 src/wikilinks/noteTemplate.ts diff --git a/src/filesystem/providers/localFilesystemProvider.ts b/src/filesystem/providers/localFilesystemProvider.ts index a9a72bb..85c02fb 100644 --- a/src/filesystem/providers/localFilesystemProvider.ts +++ b/src/filesystem/providers/localFilesystemProvider.ts @@ -78,7 +78,11 @@ export class LocalFilesystemProvider implements FileSystemProvider { return tree.map((node) => ({ id: node.id, name: node.name })); } - async createFile(parentId?: string, name?: string): Promise { + async createFile( + parentId?: string, + name?: string, + content = '', + ): Promise { const parent = parentId ?? this.rootPath; let fileName: string; if (!name || !name.trim()) { @@ -102,7 +106,7 @@ export class LocalFilesystemProvider implements FileSystemProvider { if (await this.fs.fileExists(fullPath)) { throw new Error(`A file named "${fileName}" already exists`); } - await this.fs.writeFile(fullPath, ''); + await this.fs.writeFile(fullPath, content); } catch (error) { throw new Error( `Could not create file: ${error instanceof Error ? error.message : String(error)}`, diff --git a/src/filesystem/providers/syncedVaultFilesystemProvider.ts b/src/filesystem/providers/syncedVaultFilesystemProvider.ts index 84e76aa..51006e4 100644 --- a/src/filesystem/providers/syncedVaultFilesystemProvider.ts +++ b/src/filesystem/providers/syncedVaultFilesystemProvider.ts @@ -37,8 +37,14 @@ export class SyncedVaultFilesystemProvider extends LocalFilesystemProvider { return entryId.startsWith(prefix) ? entryId.slice(prefix.length) : entryId; } - async createFile(parentId?: string, name?: string): Promise { - const entry = await super.createFile(parentId, name); + async createFile( + parentId?: string, + name?: string, + content?: string, + ): Promise { + // Write to disk first, then push — so the remote doc carries the initial + // contents rather than being created empty and filled by a later sync. + const entry = await super.createFile(parentId, name, content); // Creates the remote file doc + a snapshot entry with its uri. await this.engine.pushFile(this.vault, this.rootPath, this.rel(entry.id)); return entry; diff --git a/src/filesystem/types.ts b/src/filesystem/types.ts index a63551e..bc6a90c 100644 --- a/src/filesystem/types.ts +++ b/src/filesystem/types.ts @@ -57,7 +57,12 @@ export type FileSystemProvider = { * Create a new persisted entry in this source (e.g. a new Habitat docs * record). The shell optimistically inserts it, then refreshes the listing. */ - createFile(parentId?: string, name?: string): Promise; + createFile( + parentId?: string, + name?: string, + /** Initial file contents. Defaults to empty. */ + content?: string, + ): Promise; /** * Create a new directory in this source. * Providers without directory support may throw or no-op. diff --git a/src/wikilinks/WikiLinkVaultContext.tsx b/src/wikilinks/WikiLinkVaultContext.tsx index fa29d8a..1c208a3 100644 --- a/src/wikilinks/WikiLinkVaultContext.tsx +++ b/src/wikilinks/WikiLinkVaultContext.tsx @@ -33,6 +33,7 @@ import { type WikiLinkIndex, } from './wikiLinkIndex'; import { baseName, dirName, relPathFrom } from './paths'; +import { initialNoteContent } from './noteTemplate'; type IndexMap = ReadonlyMap; @@ -182,9 +183,11 @@ export function useWikiLinkDocument( if (relDir && !index?.dirs.has(relDir)) { await provider.createDirectory(relDir, provider.rootId); } + const fileName = baseName(activation.entryId); const entry = await provider.createFile( dir, - baseName(activation.entryId), + fileName, + initialNoteContent(fileName), ); // Open the id the provider returned, not the one we predicted. onOpenEntry({ diff --git a/src/wikilinks/noteTemplate.test.ts b/src/wikilinks/noteTemplate.test.ts new file mode 100644 index 0000000..fd73e47 --- /dev/null +++ b/src/wikilinks/noteTemplate.test.ts @@ -0,0 +1,23 @@ +import { describe, it, expect } from 'vitest'; + +import { initialNoteContent } from './noteTemplate'; + +describe('initialNoteContent', () => { + it('seeds an H1 from the note name, without the extension', () => { + expect(initialNoteContent('Meeting notes.md')).toBe('# Meeting notes\n\n'); + }); + + it('leaves a dotted name intact apart from the .md', () => { + expect(initialNoteContent('Release v1.2.md')).toBe('# Release v1.2\n\n'); + }); + + it('keeps markdown-significant characters in the title literal', () => { + // Renders as an H1 reading "C# notes", which is what the author typed. + expect(initialNoteContent('C# notes.md')).toBe('# C# notes\n\n'); + }); + + it('returns empty content rather than a bare heading for a nameless file', () => { + expect(initialNoteContent('.md')).toBe(''); + expect(initialNoteContent(' ')).toBe(''); + }); +}); diff --git a/src/wikilinks/noteTemplate.ts b/src/wikilinks/noteTemplate.ts new file mode 100644 index 0000000..a20c755 --- /dev/null +++ b/src/wikilinks/noteTemplate.ts @@ -0,0 +1,17 @@ +/** + * Starting content for a note created by following a wikilink. + * + * A wikilink names the note the author meant to write, so that name is real + * information — seeding it as an H1 saves retyping what you just typed, and + * gives `useDocumentTitle` a heading to read. Notes created by "New File" are + * left empty on purpose: their name is a placeholder (`Untitled`), so an + * `# Untitled` heading would be noise the user has to delete. + */ +import { stripMdExtension } from './paths'; + +export function initialNoteContent(fileName: string): string { + const title = stripMdExtension(fileName).trim(); + if (!title) return ''; + // Trailing blank line so the cursor starts in the body, not on the heading. + return `# ${title}\n\n`; +} -- 2.51.2