Conversation
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
as-notes | bf84b81 | Commit Preview URL Branch Preview URL |
Apr 01 2026, 11:05 AM |
There was a problem hiding this comment.
Pull request overview
This PR extends the VS Code extension’s markdown “outliner mode” editing behaviors and tightens wikilink UI behavior by suppressing completions/hover/links/decorations/rename tracking when the cursor is inside inline code or fenced code blocks (including bullet-owned fences).
Changes:
- Add code-context suppression (
isPositionInsideCode) across wikilink completion, hover, document links, decorations, and rename tracking. - Expand outliner mode with branch-aware indent/outdent/move, bullet-owned fence behaviors (enter/backspace/paste/arrow), and new context keys + keybindings.
- Add/extend unit tests and update technical docs + changelog.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| vs-code-extension/src/WikilinkRenameTracker.ts | Suppresses pending rename state + rename detection when edits occur inside code. |
| vs-code-extension/src/WikilinkHoverProvider.ts | Suppresses hover tooltips when the hovered position is inside code. |
| vs-code-extension/src/WikilinkDocumentLinkProvider.ts | Skips creating clickable wikilink segments that start inside code. |
| vs-code-extension/src/WikilinkDecorationManager.ts | Skips decorating wikilink segments that start inside code. |
| vs-code-extension/src/WikilinkCompletionProvider.ts | Suppresses completion when the cursor is inside code. |
| vs-code-extension/src/test/WikilinkRenameTracker.test.ts | Adds coverage for rename-tracker suppression inside inline code. |
| vs-code-extension/src/test/WikilinkCompletionProvider.test.ts | Extends isPositionInsideCode tests for bullet-owned fenced blocks. |
| vs-code-extension/src/test/WikilinkCodeSuppression.test.ts | New integration-style tests asserting completion suppression inside code contexts. |
| vs-code-extension/src/test/OutlinerService.test.ts | Large expansion of unit tests for new outliner behaviors and fence handling. |
| vs-code-extension/src/OutlinerService.ts | Implements new outliner primitives: backspace merge, fence cursor zones, bullet-owned fence context, branch range/moves, fence boundary guards, and fence paste formatting. |
| vs-code-extension/src/extension.ts | Wires new outliner commands, selection normalization, context keys, branch move behavior, fence boundary behaviors, and paste/backspace handling. |
| vs-code-extension/src/CompletionUtils.ts | Updates isPositionInsideCode to recognize both standalone and bullet-owned fenced blocks. |
| vs-code-extension/package.json | Updates outliner keybinding when clauses and adds new keybindings (backspace / fence arrow up/down). |
| TECHNICAL.md | Documents code suppression across wikilink surfaces and the new outliner behavior set/context keys. |
| CHANGELOG.md | Adds 2.3.1 / 2.3.0 entries. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (insideFence) { | ||
| endLine = i; | ||
| if (ANY_STANDALONE_FENCE.test(line) && !BULLET_LINE.test(line)) { | ||
| insideFence = false; | ||
| } | ||
| continue; | ||
| } | ||
|
|
||
| if (isBlankLine(line)) { | ||
| if (pendingBlankStart === null) { | ||
| pendingBlankStart = i; | ||
| } | ||
| continue; | ||
| } | ||
|
|
||
| let belongsToBranch = false; | ||
|
|
||
| if (BULLET_LINE.test(line)) { | ||
| belongsToBranch = indent > rootIndent; | ||
| } else { | ||
| belongsToBranch = indent >= continuationIndent; | ||
| } | ||
|
|
||
| if (!belongsToBranch) { | ||
| break; | ||
| } | ||
|
|
||
| endLine = i; | ||
| if (pendingBlankStart !== null) { | ||
| endLine = i; | ||
| pendingBlankStart = null; | ||
| } | ||
|
|
||
| if (ANY_STANDALONE_FENCE.test(line) && !BULLET_LINE.test(line)) { | ||
| insideFence = true; | ||
| } |
There was a problem hiding this comment.
getOutlinerBranchRange can incorrectly extend the branch past a bullet-owned fenced code block. A bullet-owned fence opener (- ```...) is a BULLET_LINE so it never sets insideFence, but the closing fence line ( ````) matchesANY_STANDALONE_FENCEand currently setsinsideFence = true(line 594-596). That causes subsequent sibling bullets/paragraphs to be treated as fence content and included in the branch until another standalone fence line appears. Fix by ensuring theinsideFencestate machine only reacts to true standalone fences (e.g., ignore fence lines that are part of a bullet-owned fence viagetBulletCodeFenceContext(...)`, or explicitly start the fence region on the bullet opener and end it on the closing fence).
| const lines = Array.from({ length: editor.document.lineCount }, (_, index) => editor.document.lineAt(index).text); | ||
|
|
||
| let totalWikilinks = 0; | ||
| for (let lineIndex = 0; lineIndex < editor.document.lineCount; lineIndex++) { | ||
| const lineText = editor.document.lineAt(lineIndex).text; | ||
| const wikilinks = this.wikilinkService.extractWikilinks(lineText); | ||
| if (wikilinks.length === 0) { continue; } | ||
|
|
||
| const segments = this.wikilinkService.computeLinkSegments(wikilinks); | ||
| const segments = this.wikilinkService.computeLinkSegments(wikilinks) | ||
| .filter(segment => !isPositionInsideCode(lines, lineIndex, segment.startOffset)); | ||
| if (segments.length === 0) { continue; } |
There was a problem hiding this comment.
isPositionInsideCode(...) performs an O(lineIndex) scan from the top of the document on every call (see CompletionUtils.ts loop over i <= lineIndex). Here it’s called for every wikilink segment while rebuilding the decoration cache, which makes rebuildCacheAndDecorate worst-case O(N^2) over document lines and can become noticeably slow on large notes. Consider precomputing a per-line fenced-code state once per document/cache rebuild (and then only doing the inline backtick check on the target line), or memoizing isPositionInsideCode results by lineIndex during this pass.
| @@ -47,6 +49,10 @@ export class WikilinkDocumentLinkProvider implements vscode.DocumentLinkProvider | |||
| const segments = this.wikilinkService.computeLinkSegments(wikilinks); | |||
|
|
|||
| for (const segment of segments) { | |||
| if (isPositionInsideCode(lines, lineIndex, segment.startOffset)) { | |||
| continue; | |||
| } | |||
|
|
|||
There was a problem hiding this comment.
provideDocumentLinks calls isPositionInsideCode(lines, lineIndex, ...) for every segment. Since isPositionInsideCode scans from line 0..lineIndex each time, this can become quadratic for long documents with many links, and provideDocumentLinks is invoked frequently by VS Code. Suggest precomputing fenced-code membership per line once (or memoizing by lineIndex) and using that to skip segments, rather than rescanning from the top for each segment.
| position: vscode.Position, | ||
| _token: vscode.CancellationToken, | ||
| ): Promise<vscode.Hover | undefined> { | ||
| const lines = Array.from({ length: document.lineCount }, (_, index) => document.lineAt(index).text); |
There was a problem hiding this comment.
provideHover builds a full lines array for the entire document on every hover request just to call isPositionInsideCode. Hover is a hot path; for large documents this can add noticeable latency/GC churn. Consider only collecting lines up to position.line (since the fence scan only needs 0..lineIndex), or refactoring isPositionInsideCode to accept a line getter / incremental scan so callers don’t need to materialize the whole document.
| const lines = Array.from({ length: document.lineCount }, (_, index) => document.lineAt(index).text); | |
| const lines = Array.from({ length: position.line + 1 }, (_, index) => document.lineAt(index).text); |
| // Track which wikilink the cursor is inside (for cursor-exit detection) | ||
| const editor = vscode.window.activeTextEditor; | ||
| if (editor && editor.document.uri.toString() === docKey) { | ||
| const cursorPos = editor.selection.active; | ||
| const lines = Array.from({ length: event.document.lineCount }, (_, index) => event.document.lineAt(index).text); | ||
| if (isPositionInsideCode(lines, cursorPos.line, Math.max(0, cursorPos.character - 1))) { | ||
| this.pendingEdit = undefined; | ||
| return; | ||
| } | ||
| const lineText = event.document.lineAt(cursorPos.line).text; | ||
| const wikilinks = this.wikilinkService.extractWikilinks(lineText); | ||
| const outermost = this.findOutermostWikilinkAtOffset(wikilinks, cursorPos.character); | ||
|
|
||
| if (outermost) { | ||
| this.pendingEdit = { | ||
| docKey, | ||
| line: cursorPos.line, | ||
| wikilinkStartPos: outermost.startPositionInText, | ||
| }; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // ── Active editor change detection ───────────────────────────────── | ||
|
|
||
| /** | ||
| * When the user switches to a different editor, check for renames | ||
| * in the previously active document. | ||
| */ | ||
| private onActiveEditorChanged(editor: vscode.TextEditor | undefined): void { | ||
| if (this.pendingEdit) { | ||
| const pendingDocKey = this.pendingEdit.docKey; | ||
| this.pendingEdit = undefined; | ||
|
|
||
| const doc = vscode.workspace.textDocuments.find( | ||
| (d) => d.uri.toString() === pendingDocKey, | ||
| ); | ||
| if (doc) { | ||
| this.checkForRenames(doc); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // ── Cursor-exit detection ────────────────────────────────────────── | ||
|
|
||
| private onSelectionChanged(event: vscode.TextEditorSelectionChangeEvent): void { | ||
| if (this.isProcessing || !this.pendingEdit) { | ||
| return; | ||
| } | ||
|
|
||
| const docKey = event.textEditor.document.uri.toString(); | ||
| if (docKey !== this.pendingEdit.docKey) { | ||
| return; | ||
| } | ||
|
|
||
| const cursorPos = event.selections[0].active; | ||
| const document = event.textEditor.document; | ||
| const lines = Array.from({ length: document.lineCount }, (_, index) => document.lineAt(index).text); | ||
| if (isPositionInsideCode(lines, cursorPos.line, Math.max(0, cursorPos.character - 1))) { | ||
| this.pendingEdit = undefined; | ||
| return; | ||
| } |
There was a problem hiding this comment.
onDocumentChanged / onSelectionChanged allocate a full lines array for the entire document on each change/selection event. These events fire very frequently during typing, so this can add avoidable overhead for large notes. Since isPositionInsideCode only needs lines 0..cursorPos.line (plus the target line), consider collecting only up to the current line, or caching a per-document line snapshot/version so repeated calls during a short typing burst don’t rebuild the whole array.
No description provided.