Skip to content

Commit b2c0d7d

Browse files
committed
Unify logging. Rename / merge edge cases
1 parent 0362099 commit b2c0d7d

19 files changed

Lines changed: 561 additions & 115 deletions

TECHNICAL.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1310,7 +1310,15 @@ Diagnostic logging is provided by `LogService` (`src/LogService.ts`), a pure Nod
13101310

13111311
**Log format:** `[ISO timestamp] [LEVEL] tag: message`
13121312

1313-
**Usage in services:** `LogService` is injected as an optional constructor parameter (defaulting to `NO_OP_LOGGER`) into `IndexService`, `IndexScanner`, `WikilinkDecorationManager`, and `WikilinkCompletionProvider`. The `extension.ts` module also uses the instance directly for lifecycle logging. A `NO_OP_LOGGER` singleton is used when logging is disabled, avoiding null checks throughout the codebase.
1313+
**Usage in services:** `LogService` is injected as an optional constructor parameter (defaulting to `NO_OP_LOGGER`) into `IndexService`, `IndexScanner`, `WikilinkDecorationManager`, `WikilinkCompletionProvider`, and other logger-aware services. The `extension.ts` module also uses the instance directly for lifecycle logging. A `NO_OP_LOGGER` singleton is used when logging is disabled, avoiding null checks throughout the codebase.
1314+
1315+
For modules that are not constructed with an injected logger (for example some inline-editor / Mermaid helpers), `LogService.ts` also exposes a shared active logger:
1316+
1317+
- `setActiveLogger(logger)` — called by `extension.ts` when logging is configured
1318+
- `getActiveLogger()` — returns the current active logger, or `NO_OP_LOGGER` when logging is off
1319+
- `formatLogError(error)` — normalises unknown exceptions into a string for logger output
1320+
1321+
This allows the extension to avoid naked `console.log/warn/error` calls while still keeping all diagnostic output behind the same logging gate.
13141322

13151323
**Timer API:** `logService.time(tag, label)` returns a closure that, when called, logs the elapsed milliseconds at INFO level — used for performance instrumentation throughout the decoration and completion pipelines.
13161324

@@ -1647,6 +1655,21 @@ For explorer renames, the old broad `staleScan()` follow-up has been replaced wi
16471655
16481656
`updateLinksInWorkspace()` no longer auto-saves affected open editors after applying workspace edits. Instead, both the in-editor and explorer rename flows now use `reindexWorkspaceUri(...)`: if an affected file is currently open, it is re-indexed from the live editor buffer via `indexFileContent(...)`; otherwise it falls back to `indexScanner.indexFile(...)`. `WikilinkRenameTracker` still re-indexes the initiating document from `document.getText()` when its URI is stable, and remaps any old source candidate URI to the post-rename or post-merge target before follow-up indexing. Combined with the direct-to-disk rewrite path for closed files, this avoids save conflicts on both open reference files and files that were previously closed, as well as attempts to reopen a source file that has just been renamed or deleted.
16491657
1658+
### Filename-level wikilink refactors
1659+
1660+
Rename propagation now also updates filenames that contain the renamed wikilink text itself, not just file contents. The shared planner in `WikilinkFilenameRefactorService.ts` scans indexed pages for filenames containing `[[oldPageName]]` and computes additional file operations such as:
1661+
1662+
1. `Topic [[Plant]].md` -> `Topic [[Tree]].md`
1663+
2. merge into an existing `Topic [[Tree]].md` when that target already exists
1664+
1665+
The planner is reused by both `WikilinkRenameTracker.ts` and `WikilinkExplorerRenameRefactorService.ts`, so in-editor renames and explorer renames follow the same rules. It provides three guarantees:
1666+
1667+
1. **Consistent rename/merge classification** - filename collisions become merges instead of failing midway.
1668+
2. **Safe ordering** - chained filename renames are topologically ordered so a rename that frees a target path can run before a dependent rename that needs that path.
1669+
3. **URI remapping for follow-up work** - candidate files selected for link rewrites or re-indexing are remapped through the filename operations so later content edits and index refreshes use the final file locations.
1670+
1671+
This keeps filename refactors, content refactors, and index updates in sync even when nested wikilinks appear inside page filenames.
1672+
16501673
### Re-entrancy guard
16511674
16521675
The `isProcessing` flag prevents document-change events fired by the rename operation itself (file renames, workspace edits) from being treated as new user edits. It is set to `true` before any rename work begins and cleared in the `finally` block.

vs-code-extension/src/BacklinkPanelProvider.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as vscode from 'vscode';
22
import { IndexService, type BacklinkChainGroup, type BacklinkChainInstance, type PageRow } from './IndexService.js';
3-
import type { LogService } from './LogService.js';
3+
import { getActiveLogger, formatLogError, type LogService } from './LogService.js';
44
import * as path from 'path';
55
import { toNotesRelativePath } from './NotesRootService.js';
66

@@ -463,7 +463,7 @@ export class BacklinkPanelProvider implements vscode.Disposable {
463463
vscode.TextEditorRevealType.InCenter,
464464
);
465465
} catch (err) {
466-
console.warn('as-notes: failed to navigate to backlink:', err);
466+
(this.logger ?? getActiveLogger()).warn('BacklinkPanel', `failed to navigate to backlink: ${formatLogError(err)}`);
467467
}
468468
}
469469

vs-code-extension/src/IgnoreService.ts

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

45
/**
56
* Reads and parses an `.asnotesignore` file (`.gitignore` syntax) and exposes
@@ -48,7 +49,7 @@ export class IgnoreService {
4849
const content = fs.readFileSync(this.ignoreFilePath, 'utf-8');
4950
instance.add(content);
5051
} catch (err) {
51-
console.warn(`as-notes: could not read ${this.ignoreFilePath}:`, err);
52+
getActiveLogger().warn('IgnoreService', `could not read ${this.ignoreFilePath}: ${formatLogError(err)}`);
5253
}
5354
return instance;
5455
}

vs-code-extension/src/IndexScanner.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as vscode from 'vscode';
22
import * as path from 'path';
33
import { IndexService, type ScanSummary } from './IndexService.js';
44
import { IgnoreService } from './IgnoreService.js';
5-
import { LogService, NO_OP_LOGGER } from './LogService.js';
5+
import { LogService, NO_OP_LOGGER, formatLogError } from './LogService.js';
66
import { toNotesRelativePath } from './NotesRootService.js';
77

88
/**
@@ -108,7 +108,7 @@ export class IndexScanner {
108108
this.logger.info('IndexScanner', `fullScan: progress ${filesIndexed}/${total} files`);
109109
}
110110
} catch (err) {
111-
console.warn(`as-notes: failed to index ${relativePath}:`, err);
111+
this.logger.warn('IndexScanner', `failed to index ${relativePath}: ${formatLogError(err)}`);
112112
}
113113
}
114114

@@ -186,7 +186,7 @@ export class IndexScanner {
186186
summary.unchanged++;
187187
}
188188
} catch (err) {
189-
console.warn(`as-notes: failed to scan ${relativePath}:`, err);
189+
this.logger.warn('IndexScanner', `failed to scan ${relativePath}: ${formatLogError(err)}`);
190190
}
191191

192192
processed++;

vs-code-extension/src/LogService.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,3 +154,17 @@ export class LogService {
154154
* Avoids null checks throughout the codebase — callers always have a LogService.
155155
*/
156156
export const NO_OP_LOGGER = new LogService('', { enabled: false });
157+
158+
let activeLogger: LogService = NO_OP_LOGGER;
159+
160+
export function setActiveLogger(logger: LogService): void {
161+
activeLogger = logger;
162+
}
163+
164+
export function getActiveLogger(): LogService {
165+
return activeLogger;
166+
}
167+
168+
export function formatLogError(error: unknown): string {
169+
return error instanceof Error ? error.message : String(error);
170+
}

vs-code-extension/src/TaskPanelProvider.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as vscode from 'vscode';
22
import { IndexService, type TaskViewItem } from './IndexService.js';
3+
import { formatLogError, getActiveLogger } from './LogService.js';
34
import { toggleTodoLine } from './TodoToggleService.js';
45

56
// ── Provider ───────────────────────────────────────────────────────────────
@@ -88,7 +89,7 @@ export class TaskPanelProvider implements vscode.WebviewViewProvider {
8889
edit.replace(doc.uri, doc.lineAt(line).range, toggled);
8990
return vscode.workspace.applyEdit(edit).then(() => doc.save());
9091
}).then(undefined, err => {
91-
console.warn('as-notes: failed to toggle task from panel:', err);
92+
getActiveLogger().warn('TaskPanel', `failed to toggle task from panel: ${formatLogError(err)}`);
9293
});
9394
break;
9495
}

vs-code-extension/src/WikilinkExplorerRenameRefactorService.ts

Lines changed: 83 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ import {
1010
import { toNotesRelativePath } from './NotesRootService.js';
1111
import { reindexWorkspaceUri, updateLinksInWorkspace } from './WikilinkRefactorService.js';
1212
import { withWikilinkRenameProgress } from './WikilinkRenameProgressService.js';
13+
import {
14+
collectFilenameRefactorOperations,
15+
remapUrisForFileOperations,
16+
} from './WikilinkFilenameRefactorService.js';
1317

1418
interface ExplorerRenameFile {
1519
oldUri: vscode.Uri;
@@ -20,7 +24,7 @@ interface ExplorerRenameRefactorDeps {
2024
files: ExplorerRenameFile[];
2125
renameTrackerIsRenaming: boolean;
2226
wikilinkService: WikilinkService;
23-
indexService: Pick<IndexService, 'findPagesByFilename' | 'removePage' | 'findPagesLinkingToPageNames' | 'indexFileContent'>;
27+
indexService: Pick<IndexService, 'findPagesByFilename' | 'removePage' | 'findPagesLinkingToPageNames' | 'indexFileContent' | 'getAllPages'>;
2428
indexScanner: Pick<IndexScanner, 'staleScan' | 'indexFile'>;
2529
notesRootPath?: string;
2630
safeSaveToFile: () => boolean;
@@ -124,26 +128,72 @@ export async function handleExplorerRenameRefactors({
124128
const summary = linkRenames
125129
.map(r => `[[${r.oldPageName}]] → [[${r.newPageName}]]`)
126130
.join(', ');
131+
const rootUri = notesRootPath
132+
? vscode.Uri.file(notesRootPath)
133+
: vscode.workspace.workspaceFolders?.[0]?.uri;
134+
const filenameRefactorPlan = rootUri && indexService.getAllPages
135+
? collectFilenameRefactorOperations(
136+
linkRenames,
137+
indexService.getAllPages(),
138+
rootUri,
139+
{
140+
excludePaths: files.map(file => {
141+
const uri = file.newUri;
142+
return notesRootPath
143+
? toNotesRelativePath(notesRootPath, uri.fsPath)
144+
: vscode.workspace.asRelativePath(uri, false);
145+
}),
146+
},
147+
)
148+
: { fileRenames: [], fileMerges: [] };
149+
const extraFilenameChanges = filenameRefactorPlan.fileRenames.length + filenameRefactorPlan.fileMerges.length;
127150
const msg = linkRenames.length === 1
128-
? `Update all ${summary} references?`
129-
: `Update references for ${linkRenames.length} renamed files? ${summary}`;
151+
? `Update all ${summary} references?${extraFilenameChanges > 0 ? ` Also update ${extraFilenameChanges} filename reference(s).` : ''}`
152+
: `Update references for ${linkRenames.length} renamed files? ${summary}${extraFilenameChanges > 0 ? ` Also update ${extraFilenameChanges} filename reference(s).` : ''}`;
130153
const choice = await vscode.window.showInformationMessage(msg, 'Yes', 'No');
131154
if (choice !== 'Yes') { return; }
132155

133156
await withWikilinkRenameProgress('AS Notes: Updating wikilink references', async (progress) => {
134-
const rootUri = notesRootPath
135-
? vscode.Uri.file(notesRootPath)
136-
: vscode.workspace.workspaceFolders?.[0]?.uri;
137157
const candidateUris = rootUri
138158
? indexService.findPagesLinkingToPageNames(linkRenames.map(rename => rename.oldPageName))
139159
.map(page => vscode.Uri.joinPath(rootUri, page.path))
140160
: [];
141161

162+
progress.report('Preparing rename operations');
163+
for (const merge of filenameRefactorPlan.fileMerges) {
164+
const sourceDoc = await vscode.workspace.openTextDocument(merge.oldUri);
165+
const targetDoc = await vscode.workspace.openTextDocument(merge.newUri);
166+
167+
const mergedContent = new FrontMatterService().mergeDocuments(
168+
targetDoc.getText(),
169+
sourceDoc.getText(),
170+
);
171+
172+
const edit = new vscode.WorkspaceEdit();
173+
const fullRange = new vscode.Range(
174+
targetDoc.lineAt(0).range.start,
175+
targetDoc.lineAt(targetDoc.lineCount - 1).range.end,
176+
);
177+
edit.replace(merge.newUri, fullRange, mergedContent);
178+
await vscode.workspace.applyEdit(edit);
179+
await vscode.workspace.fs.delete(merge.oldUri);
180+
}
181+
182+
for (const rename of filenameRefactorPlan.fileRenames) {
183+
await vscode.workspace.fs.rename(rename.oldUri, rename.newUri, { overwrite: false });
184+
}
185+
186+
const rewriteCandidateUris = remapUrisForFileOperations(
187+
candidateUris,
188+
filenameRefactorPlan.fileRenames,
189+
filenameRefactorPlan.fileMerges,
190+
);
191+
142192
progress.report('Updating links across workspace');
143193
const affectedUris = await updateLinksInWorkspace(
144194
wikilinkService,
145195
linkRenames,
146-
candidateUris.length > 0 ? { candidateUris } : undefined,
196+
rewriteCandidateUris.length > 0 ? { candidateUris: rewriteCandidateUris } : undefined,
147197
);
148198

149199
progress.report('Refreshing index');
@@ -156,6 +206,32 @@ export async function handleExplorerRenameRefactors({
156206
});
157207
} catch { /* best effort */ }
158208
}
209+
for (const merge of filenameRefactorPlan.fileMerges) {
210+
const oldPath = notesRootPath
211+
? toNotesRelativePath(notesRootPath, merge.oldUri.fsPath)
212+
: vscode.workspace.asRelativePath(merge.oldUri, false);
213+
indexService.removePage(oldPath);
214+
try {
215+
await reindexWorkspaceUri(merge.newUri, {
216+
indexService,
217+
indexScanner,
218+
notesRootPath,
219+
});
220+
} catch { /* best effort */ }
221+
}
222+
for (const rename of filenameRefactorPlan.fileRenames) {
223+
const oldPath = notesRootPath
224+
? toNotesRelativePath(notesRootPath, rename.oldUri.fsPath)
225+
: vscode.workspace.asRelativePath(rename.oldUri, false);
226+
indexService.removePage(oldPath);
227+
try {
228+
await reindexWorkspaceUri(rename.newUri, {
229+
indexService,
230+
indexScanner,
231+
notesRootPath,
232+
});
233+
} catch { /* best effort */ }
234+
}
159235
if (safeSaveToFile()) {
160236
refreshProviders();
161237
}

0 commit comments

Comments
 (0)