Skip to content

Outliner behaviours#23

Merged
gbro3n merged 4 commits intomainfrom
outliner-behaviours
Apr 1, 2026
Merged

Outliner behaviours#23
gbro3n merged 4 commits intomainfrom
outliner-behaviours

Conversation

@gbro3n
Copy link
Copy Markdown
Member

@gbro3n gbro3n commented Apr 1, 2026

No description provided.

Copilot AI review requested due to automatic review settings April 1, 2026 11:12
@cloudflare-workers-and-pages
Copy link
Copy Markdown
Contributor

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

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

@gbro3n gbro3n merged commit 014dd12 into main Apr 1, 2026
2 checks passed
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +561 to +596
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;
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +191 to +201
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; }
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 38 to +55
@@ -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;
}

Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
position: vscode.Position,
_token: vscode.CancellationToken,
): Promise<vscode.Hover | undefined> {
const lines = Array.from({ length: document.lineCount }, (_, index) => document.lineAt(index).text);
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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);

Copilot uses AI. Check for mistakes.
Comment on lines 189 to +250
// 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;
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants