From 26098abf2629828aa57c84fec5c428872a840278 Mon Sep 17 00:00:00 2001 From: elliot Date: Wed, 24 Sep 2025 15:53:45 -0400 Subject: [PATCH 01/12] use statementRangeProvider in Positron to execute statements at cursor in VE --- apps/vscode/src/providers/cell/commands.ts | 37 +++++++++++++++++++--- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/apps/vscode/src/providers/cell/commands.ts b/apps/vscode/src/providers/cell/commands.ts index 11a07a39..420a9667 100644 --- a/apps/vscode/src/providers/cell/commands.ts +++ b/apps/vscode/src/providers/cell/commands.ts @@ -51,6 +51,10 @@ import { ExtensionHost } from "../../host"; import { hasHooks } from "../../host/hooks"; import { isKnitrDocument } from "../../host/executors"; import { commands } from "vscode"; +import { virtualDocForCode, withVirtualDocUri } from "../../vdoc/vdoc"; +import { embeddedLanguage } from "../../vdoc/languages"; +import { Uri } from "vscode"; +import { StatementRange } from "positron"; export function cellCommands(host: ExtensionHost, engine: MarkdownEngine): Command[] { return [ @@ -348,17 +352,40 @@ class RunCurrentCommand extends RunCommand implements Command { await activateIfRequired(editor); } } - } else { - // if the selection is empty take the whole line, otherwise take the selected text exactly let action: CodeViewSelectionAction | undefined; - if (selection.length <= 0) { - if (activeBlock) { - selection = lines(activeBlock.code)[context.selection.start.line]; + + // if the selection is empty and we are in Positron: + // try to get the statement's range and use that as the selection + if (selection.length <= 0 && activeBlock && hasHooks()) { + const language = embeddedLanguage(activeBlock.language); + const vdoc = virtualDocForCode(lines(activeBlock.code), language!); + const parentUri = Uri.file(editor.document.fileName); + if (vdoc) { + const result = await withVirtualDocUri(vdoc, parentUri, "statementRange", async (uri) => { + return await commands.executeCommand( + "vscode.executeStatementRangeProvider", + uri, + context.selection.start + ); + }); + const { range: { start, end } } = result + const slicedLines = lines(activeBlock.code).slice(start.line, end.line + 1) + slicedLines[0] = slicedLines[0].slice(start.character) + slicedLines[slicedLines.length - 1] = slicedLines[slicedLines.length - 1].slice(0, end.character) + + selection = slicedLines.join('\n') action = "nextline"; } } + // if the selection is still empty: + // take the whole line as the selection + if (selection.length <= 0 && activeBlock) { + selection = lines(activeBlock.code)[context.selection.start.line]; + action = "nextline"; + } + // run code const executor = await this.cellExecutorForLanguage(context.activeLanguage, editor.document, this.engine_); if (executor) { From 3a513f02d98c74e70460d706127b129c3dc6af45 Mon Sep 17 00:00:00 2001 From: elliot Date: Fri, 3 Oct 2025 15:31:25 -0400 Subject: [PATCH 02/12] WIP fix advancing to next statement I don't think my changes to `codeViewSetBlockSelection` work. It seems that `navigateToPos` does not work inside codeMirror? That must be why only "nextline" works in the command - it doesn't use `navigateToPos` it uses a special codeView thing. --- apps/vscode/src/providers/cell/commands.ts | 33 +++++++++++++++++++--- packages/editor-types/src/codeview.ts | 2 +- packages/editor/src/api/codeview.ts | 21 ++++++++++---- 3 files changed, 45 insertions(+), 11 deletions(-) diff --git a/apps/vscode/src/providers/cell/commands.ts b/apps/vscode/src/providers/cell/commands.ts index 420a9667..1917be56 100644 --- a/apps/vscode/src/providers/cell/commands.ts +++ b/apps/vscode/src/providers/cell/commands.ts @@ -358,10 +358,10 @@ class RunCurrentCommand extends RunCommand implements Command { // if the selection is empty and we are in Positron: // try to get the statement's range and use that as the selection if (selection.length <= 0 && activeBlock && hasHooks()) { - const language = embeddedLanguage(activeBlock.language); - const vdoc = virtualDocForCode(lines(activeBlock.code), language!); - const parentUri = Uri.file(editor.document.fileName); + const codeLines = lines(activeBlock.code) + const vdoc = virtualDocForCode(codeLines, embeddedLanguage(activeBlock.language)!); if (vdoc) { + const parentUri = Uri.file(editor.document.fileName); const result = await withVirtualDocUri(vdoc, parentUri, "statementRange", async (uri) => { return await commands.executeCommand( "vscode.executeStatementRangeProvider", @@ -376,6 +376,29 @@ class RunCurrentCommand extends RunCommand implements Command { selection = slicedLines.join('\n') action = "nextline"; + + // ref: https://github.com/posit-dev/positron/blob/main/src/vs/workbench/contrib/positronConsole/browser/positronConsoleActions.ts#L428 + if (end.line + 1 <= codeLines.length) { + const nextStatementRange = await withVirtualDocUri(vdoc, parentUri, "statementRange", async (uri) => { + return await commands.executeCommand( + "vscode.executeStatementRangeProvider", + uri, + new Position(end.line + 1, 1) + ); + }); + const nextStatement = nextStatementRange.range; + if (nextStatement.start.line > end.line) { + // If the next statement's start is after this statement's end, + // then move to the start of the next statement. + action = nextStatement.start + } else if (nextStatement.end.line > end.line) { + // If the above condition failed, but the next statement's end + // is after this statement's end, assume we are exiting some + // nested scope (like running an individual line of an R + // function) and move to the end of the next statement. + action = nextStatement.end + } + } } } @@ -392,8 +415,10 @@ class RunCurrentCommand extends RunCommand implements Command { await executeInteractive(executor, [selection], editor.document); // advance cursor if necessary + // if (action) { - editor.setBlockSelection(context, "nextline"); + console.log('action!!!', action, this.id) + await editor.setBlockSelection(context, action); } } } diff --git a/packages/editor-types/src/codeview.ts b/packages/editor-types/src/codeview.ts index 2977f404..e40da3ba 100644 --- a/packages/editor-types/src/codeview.ts +++ b/packages/editor-types/src/codeview.ts @@ -34,7 +34,7 @@ export interface CodeViewActiveBlockContext { selectedText: string; } -export type CodeViewSelectionAction = "nextline" | "nextblock" | "prevblock"; +export type CodeViewSelectionAction = "nextline" | "nextblock" | "prevblock" | { line: number, character: number }; export interface CodeViewCellContext { filepath: string; diff --git a/packages/editor/src/api/codeview.ts b/packages/editor/src/api/codeview.ts index 25e4df42..201ccfe2 100644 --- a/packages/editor/src/api/codeview.ts +++ b/packages/editor/src/api/codeview.ts @@ -205,12 +205,23 @@ export function codeViewSetBlockSelection( context: CodeViewActiveBlockContext, action: CodeViewSelectionAction ) { - - const activeIndex = context.blocks.findIndex(block => block.active); if (activeIndex !== -1) { - if (action === "nextline") { + if (typeof action === 'object') { + // convert action line and character in code block space to pos in prosemirror space + const block = context.blocks[activeIndex] + const code = lines(block.code) + if (action.line > code.length) throw 'trying to move cursor outside block!' + let pos = block.pos + for (let i = 0; i <= action.line; i++) { + pos += code[i].length + } + pos += action.character + + console.log('yoooo', pos, navigateToPos(view, pos, false)); + } + else if (action === "nextline") { const tr = view.state.tr; tr.setMeta(kCodeViewNextLineTransaction, true); view.dispatch(tr); @@ -222,13 +233,11 @@ export function codeViewSetBlockSelection( navigatePos = context.blocks[activeIndex - 1]?.pos; } if (navigatePos) { + console.log('yoooo22', navigatePos) navigateToPos(view, navigatePos!, false); } } } - - - } From 81924682e614a83f9ad2a0ea4b46cba38f70f08b Mon Sep 17 00:00:00 2001 From: elliot Date: Fri, 3 Oct 2025 16:45:17 -0400 Subject: [PATCH 03/12] fix advancing to next statement --- apps/vscode/src/providers/cell/commands.ts | 14 +++++------- .../src/behaviors/trackselection.ts | 22 +++++++++---------- packages/editor/src/api/codeview.ts | 10 +++++---- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/apps/vscode/src/providers/cell/commands.ts b/apps/vscode/src/providers/cell/commands.ts index 1917be56..1e826fa9 100644 --- a/apps/vscode/src/providers/cell/commands.ts +++ b/apps/vscode/src/providers/cell/commands.ts @@ -377,28 +377,26 @@ class RunCurrentCommand extends RunCommand implements Command { selection = slicedLines.join('\n') action = "nextline"; - // ref: https://github.com/posit-dev/positron/blob/main/src/vs/workbench/contrib/positronConsole/browser/positronConsoleActions.ts#L428 + // BEGIN ref: https://github.com/posit-dev/positron/blob/main/src/vs/workbench/contrib/positronConsole/browser/positronConsoleActions.ts#L428 + // strategy from Positron using `StatementRangeProvider` to find range of next statement + // and move cursor based on that. if (end.line + 1 <= codeLines.length) { const nextStatementRange = await withVirtualDocUri(vdoc, parentUri, "statementRange", async (uri) => { return await commands.executeCommand( "vscode.executeStatementRangeProvider", uri, - new Position(end.line + 1, 1) + new Position(end.line + 1, 1) // look for statement at line after current statement ); }); const nextStatement = nextStatementRange.range; if (nextStatement.start.line > end.line) { - // If the next statement's start is after this statement's end, - // then move to the start of the next statement. action = nextStatement.start + // the nextStatement may start before & end after the current statement if e.g. inside a function: } else if (nextStatement.end.line > end.line) { - // If the above condition failed, but the next statement's end - // is after this statement's end, assume we are exiting some - // nested scope (like running an individual line of an R - // function) and move to the end of the next statement. action = nextStatement.end } } + // END ref. } } diff --git a/packages/editor-codemirror/src/behaviors/trackselection.ts b/packages/editor-codemirror/src/behaviors/trackselection.ts index bfcd073f..79a4b84f 100644 --- a/packages/editor-codemirror/src/behaviors/trackselection.ts +++ b/packages/editor-codemirror/src/behaviors/trackselection.ts @@ -27,7 +27,7 @@ import { DispatchEvent, codeViewCellContext, kCodeViewNextLineTransaction } from import { Behavior, BehaviorContext, State } from "."; // track the selection in prosemirror -export function trackSelectionBehavior(context: BehaviorContext) : Behavior { +export function trackSelectionBehavior(context: BehaviorContext): Behavior { let unsubscribe: VoidFunction; @@ -50,32 +50,32 @@ export function trackSelectionBehavior(context: BehaviorContext) : Behavior { unsubscribe = context.pmContext.events.subscribe(DispatchEvent, (tr: Transaction | undefined) => { if (tr) { // track selection changes that occur when we don't have focus - if (!cmView.hasFocus && tr.selectionSet && !tr.docChanged && (tr.selection instanceof TextSelection)) { + if (tr.selectionSet && !tr.docChanged && (tr.selection instanceof TextSelection)) { const cmSelection = asCodeMirrorSelection(context.view, cmView, context.getPos); context.withState(State.Updating, () => { if (cmSelection) { cmView.dispatch({ selection: cmSelection }); } else { - cmView.dispatch({ selection: EditorSelection.single(0)}) - } + cmView.dispatch({ selection: EditorSelection.single(0) }) + } }) } else if (tr.getMeta(kCodeViewNextLineTransaction) === true) { // NOTE: this is a special directive to advance to the next line. as distinct // from the block above it is not a reporting of a change in the PM selection - // but rather an instruction to move the CM selection to the next line. as + // but rather an instruction to move the CM selection to the next line. as // such we do not encose the code in State.Updating, because we want an update // to the PM selection to occur const cmSelection = asCodeMirrorSelection(context.view, cmView, context.getPos); if (cmSelection) { if (cursorLineDown(cmView)) { cursorLineStart(cmView); - } + } } - // for other selection changes + // for other selection changes } else if (cmView.hasFocus && tr.selectionSet && (tr.selection instanceof TextSelection)) { codeViewAssist(); } - } + } }); }, @@ -91,7 +91,7 @@ export const asCodeMirrorSelection = ( cmView: EditorView, getPos: (() => number) | boolean ) => { - if (typeof(getPos) === "function") { + if (typeof (getPos) === "function") { const offset = getPos() + 1; const node = pmView.state.doc.nodeAt(getPos()); if (node) { @@ -104,8 +104,8 @@ export const asCodeMirrorSelection = ( } else if (selection.from <= cmRange.from && selection.to >= cmRange.to) { return EditorSelection.single(0, cmView.state.doc.length); } - + } } return undefined; -} \ No newline at end of file +} diff --git a/packages/editor/src/api/codeview.ts b/packages/editor/src/api/codeview.ts index 201ccfe2..8e372b83 100644 --- a/packages/editor/src/api/codeview.ts +++ b/packages/editor/src/api/codeview.ts @@ -211,15 +211,17 @@ export function codeViewSetBlockSelection( if (typeof action === 'object') { // convert action line and character in code block space to pos in prosemirror space const block = context.blocks[activeIndex] + // asummes the meta line looks like this: + const metaLine = '{' + block.language + '}\n' const code = lines(block.code) if (action.line > code.length) throw 'trying to move cursor outside block!' - let pos = block.pos - for (let i = 0; i <= action.line; i++) { - pos += code[i].length + let pos = block.pos + metaLine.length + for (let i = 0; i < action.line; i++) { + pos += code[i].length + 1 } pos += action.character - console.log('yoooo', pos, navigateToPos(view, pos, false)); + navigateToPos(view, pos, false) } else if (action === "nextline") { const tr = view.state.tr; From f688c6f39e79f315ee3320a0ed05c83d30281219 Mon Sep 17 00:00:00 2001 From: elliot Date: Tue, 21 Oct 2025 10:50:33 -0400 Subject: [PATCH 04/12] WIP move to next statement --- apps/vscode/src/providers/cell/commands.ts | 36 ++++++++++++++-------- packages/editor/src/api/codeview.ts | 1 - 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/apps/vscode/src/providers/cell/commands.ts b/apps/vscode/src/providers/cell/commands.ts index 1e826fa9..07671160 100644 --- a/apps/vscode/src/providers/cell/commands.ts +++ b/apps/vscode/src/providers/cell/commands.ts @@ -343,8 +343,11 @@ class RunCurrentCommand extends RunCommand implements Command { let selection = context.selectedText; const activeBlock = context.blocks.find(block => block.active); + // idea: first check that we are in Positron + // and leave the others untouched + // if the selection is empty and this isn't a knitr document then it resolves to run cell - if (selection.length <= 0 && !isKnitrDocument(editor.document, this.engine_)) { + if (false) { if (activeBlock) { const executor = await this.cellExecutorForLanguage(activeBlock.language, editor.document, this.engine_); if (executor) { @@ -362,37 +365,46 @@ class RunCurrentCommand extends RunCommand implements Command { const vdoc = virtualDocForCode(codeLines, embeddedLanguage(activeBlock.language)!); if (vdoc) { const parentUri = Uri.file(editor.document.fileName); + const injectedLines = (vdoc.language?.inject?.length ?? 0) + + const positionIntoVdoc = (p: { line: number, character: number }) => + new Position(p.line + injectedLines, p.character) + const positionOutOfVdoc = (p: { line: number, character: number }) => + new Position(p.line - injectedLines, p.character) + const result = await withVirtualDocUri(vdoc, parentUri, "statementRange", async (uri) => { return await commands.executeCommand( "vscode.executeStatementRangeProvider", uri, - context.selection.start + positionIntoVdoc(context.selection.start) ); }); - const { range: { start, end } } = result - const slicedLines = lines(activeBlock.code).slice(start.line, end.line + 1) - slicedLines[0] = slicedLines[0].slice(start.character) - slicedLines[slicedLines.length - 1] = slicedLines[slicedLines.length - 1].slice(0, end.character) + const { range, code } = result + if (code === undefined) return + const adjustedEnd = positionOutOfVdoc(range.end) - selection = slicedLines.join('\n') + selection = code action = "nextline"; // BEGIN ref: https://github.com/posit-dev/positron/blob/main/src/vs/workbench/contrib/positronConsole/browser/positronConsoleActions.ts#L428 // strategy from Positron using `StatementRangeProvider` to find range of next statement // and move cursor based on that. - if (end.line + 1 <= codeLines.length) { + if (adjustedEnd.line + 1 <= codeLines.length) { const nextStatementRange = await withVirtualDocUri(vdoc, parentUri, "statementRange", async (uri) => { return await commands.executeCommand( "vscode.executeStatementRangeProvider", uri, - new Position(end.line + 1, 1) // look for statement at line after current statement + positionIntoVdoc(new Position(adjustedEnd.line + 1, 1)) // look for statement at line after current statement ); }); - const nextStatement = nextStatementRange.range; - if (nextStatement.start.line > end.line) { + const nextStatement = { + start: positionOutOfVdoc(nextStatementRange.range.start), + end: positionOutOfVdoc(nextStatementRange.range.end) + }; + if (nextStatement.start.line > adjustedEnd.line) { action = nextStatement.start // the nextStatement may start before & end after the current statement if e.g. inside a function: - } else if (nextStatement.end.line > end.line) { + } else if (nextStatement.end.line > adjustedEnd.line) { action = nextStatement.end } } diff --git a/packages/editor/src/api/codeview.ts b/packages/editor/src/api/codeview.ts index 8e372b83..3f664d11 100644 --- a/packages/editor/src/api/codeview.ts +++ b/packages/editor/src/api/codeview.ts @@ -235,7 +235,6 @@ export function codeViewSetBlockSelection( navigatePos = context.blocks[activeIndex - 1]?.pos; } if (navigatePos) { - console.log('yoooo22', navigatePos) navigateToPos(view, navigatePos!, false); } } From f8347edd319a64d699bd15243f5d47be070f51c6 Mon Sep 17 00:00:00 2001 From: elliot Date: Fri, 31 Oct 2025 14:53:00 -0400 Subject: [PATCH 05/12] Working Positron VE statement range execution in r and python --- apps/vscode/src/providers/cell/commands.ts | 138 +++++++++++---------- 1 file changed, 70 insertions(+), 68 deletions(-) diff --git a/apps/vscode/src/providers/cell/commands.ts b/apps/vscode/src/providers/cell/commands.ts index 07671160..1531abea 100644 --- a/apps/vscode/src/providers/cell/commands.ts +++ b/apps/vscode/src/providers/cell/commands.ts @@ -263,7 +263,19 @@ class RunPreviousCellCommand extends RunCommand implements Command { } } +// More permissive type than `Position` so its easier to construct via a literal +type LineAndCharPos = { line: number, character: number } +// More permissive type than `Range` so its easier to construct via a literal +type LineAndCharRange = { start: LineAndCharPos, end: LineAndCharPos } + +function extractRangeFromCode(code: string, range: LineAndCharRange): string { + const extractedRange = lines(code).slice(range.start.line, range.end.line + 1) + extractedRange[0] = extractedRange[0].slice(range.start.character) + extractedRange[extractedRange.length - 1] = extractedRange[extractedRange.length - 1].slice(0, range.end.character) + return extractedRange.join('\n') +} +// Run the code at the cursor class RunCurrentCommand extends RunCommand implements Command { constructor( host: ExtensionHost, @@ -340,95 +352,85 @@ class RunCurrentCommand extends RunCommand implements Command { context: CodeViewActiveBlockContext ): Promise { // get selection and active block - let selection = context.selectedText; + const selection = context.selectedText; const activeBlock = context.blocks.find(block => block.active); - // idea: first check that we are in Positron - // and leave the others untouched - - // if the selection is empty and this isn't a knitr document then it resolves to run cell - if (false) { - if (activeBlock) { - const executor = await this.cellExecutorForLanguage(activeBlock.language, editor.document, this.engine_); - if (executor) { - await executeInteractive(executor, [activeBlock.code], editor.document); - await activateIfRequired(editor); - } + const exec = async (action: CodeViewSelectionAction, selection: string) => { + const executor = await this.cellExecutorForLanguage(context.activeLanguage, editor.document, this.engine_); + if (executor) { + await executeInteractive(executor, [selection], editor.document); + await editor.setBlockSelection(context, action); } - } else { - let action: CodeViewSelectionAction | undefined; + } - // if the selection is empty and we are in Positron: - // try to get the statement's range and use that as the selection - if (selection.length <= 0 && activeBlock && hasHooks()) { + // if in Positron + if (hasHooks()) { + if (activeBlock && selection.length <= 0) { const codeLines = lines(activeBlock.code) const vdoc = virtualDocForCode(codeLines, embeddedLanguage(activeBlock.language)!); if (vdoc) { const parentUri = Uri.file(editor.document.fileName); const injectedLines = (vdoc.language?.inject?.length ?? 0) - const positionIntoVdoc = (p: { line: number, character: number }) => + const positionIntoVdoc = (p: LineAndCharPos) => new Position(p.line + injectedLines, p.character) - const positionOutOfVdoc = (p: { line: number, character: number }) => + const positionOutOfVdoc = (p: LineAndCharPos) => new Position(p.line - injectedLines, p.character) - - const result = await withVirtualDocUri(vdoc, parentUri, "statementRange", async (uri) => { - return await commands.executeCommand( - "vscode.executeStatementRangeProvider", - uri, - positionIntoVdoc(context.selection.start) - ); - }); - const { range, code } = result - if (code === undefined) return - const adjustedEnd = positionOutOfVdoc(range.end) - - selection = code - action = "nextline"; - - // BEGIN ref: https://github.com/posit-dev/positron/blob/main/src/vs/workbench/contrib/positronConsole/browser/positronConsoleActions.ts#L428 - // strategy from Positron using `StatementRangeProvider` to find range of next statement - // and move cursor based on that. - if (adjustedEnd.line + 1 <= codeLines.length) { - const nextStatementRange = await withVirtualDocUri(vdoc, parentUri, "statementRange", async (uri) => { + const rangeOutOfVdoc = (r: Range): LineAndCharRange => ({ + start: positionOutOfVdoc(r.start), + end: positionOutOfVdoc(r.end) + }) + const getStatementRange = async (pos: LineAndCharPos) => { + const result = await withVirtualDocUri(vdoc, parentUri, "statementRange", async (uri) => { return await commands.executeCommand( "vscode.executeStatementRangeProvider", uri, - positionIntoVdoc(new Position(adjustedEnd.line + 1, 1)) // look for statement at line after current statement + positionIntoVdoc(pos) ); }); - const nextStatement = { - start: positionOutOfVdoc(nextStatementRange.range.start), - end: positionOutOfVdoc(nextStatementRange.range.end) - }; - if (nextStatement.start.line > adjustedEnd.line) { - action = nextStatement.start - // the nextStatement may start before & end after the current statement if e.g. inside a function: - } else if (nextStatement.end.line > adjustedEnd.line) { - action = nextStatement.end + return rangeOutOfVdoc(result.range) + } + + const range = await getStatementRange(context.selection.start) + const code = extractRangeFromCode(activeBlock.code, range) + + // BEGIN ref: https://github.com/posit-dev/positron/blob/main/src/vs/workbench/contrib/positronConsole/browser/positronConsoleActions.ts#L428 + // strategy from Positron using `StatementRangeProvider` to find range of next statement + // and move cursor based on that. + if (range.end.line + 1 <= codeLines.length) { + // get range of statement at line after current statement) + const nextRange = await getStatementRange(new Position(range.end.line + 1, 1)) + + if (nextRange.start.line > range.end.line) { + exec(nextRange.start, code) + // the next statement range may start before & end after the current statement if e.g. inside a function: + } else if (nextRange.end.line > range.end.line) { + exec(nextRange.end, code) + } else { + exec("nextline", code) } + } else { + exec("nextline", code) } // END ref. } } - - // if the selection is still empty: - // take the whole line as the selection - if (selection.length <= 0 && activeBlock) { - selection = lines(activeBlock.code)[context.selection.start.line]; - action = "nextline"; - } - - // run code - const executor = await this.cellExecutorForLanguage(context.activeLanguage, editor.document, this.engine_); - if (executor) { - await executeInteractive(executor, [selection], editor.document); - - // advance cursor if necessary - // - if (action) { - console.log('action!!!', action, this.id) - await editor.setBlockSelection(context, action); + // if not in Positron + } else { + // if the selection is empty and this isn't a knitr document then it resolves to run cell + if (selection.length <= 0 && !isKnitrDocument(editor.document, this.engine_)) { + if (activeBlock) { + const executor = await this.cellExecutorForLanguage(activeBlock.language, editor.document, this.engine_); + if (executor) { + await executeInteractive(executor, [activeBlock.code], editor.document); + await activateIfRequired(editor); + } + } + } else { + if (selection.length > 0) { + exec("nextline", selection) + } else if (activeBlock) { // if the selection is empty take the whole line as the selection + exec("nextline", lines(activeBlock.code)[context.selection.start.line]) } } } @@ -444,7 +446,7 @@ class RunSelectionCommand extends RunCurrentCommand implements Command { } - +// Run Cell and Advance class RunCurrentAdvanceCommand extends RunCommand implements Command { constructor(host: ExtensionHost, engine: MarkdownEngine) { super(host, engine); From e944e72c3af88ee638aea2781aedff351a51dd75 Mon Sep 17 00:00:00 2001 From: elliot Date: Tue, 4 Nov 2025 14:41:14 -0500 Subject: [PATCH 06/12] Add comment clarifying `codeViewExecute` --- apps/vscode/src/providers/editor/codeview.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/vscode/src/providers/editor/codeview.ts b/apps/vscode/src/providers/editor/codeview.ts index 13a990ce..d2500a6e 100644 --- a/apps/vscode/src/providers/editor/codeview.ts +++ b/apps/vscode/src/providers/editor/codeview.ts @@ -60,6 +60,11 @@ export function vscodeCodeViewServer(_engine: MarkdownEngine, document: TextDocu async codeViewAssist(context: CodeViewCellContext) { await commands.executeCommand("quarto.codeViewAssist", context, lspRequest); }, + // This execute command is used when the user clicks an execute button on a cell in the visual editor. + // + // Note: this is NOT used when the user uses a keyboard command to execute a cell, + // that goes through VSCode commands (commands are registered in package.json), + // the keyboard command code is in apps/vscode/src/providers/cell/commands.ts. async codeViewExecute(execute: CodeViewExecute) { switch (execute) { case "cell": From 9ea8e836643d7fbbe96f9312716197a719e82c91 Mon Sep 17 00:00:00 2001 From: elliot Date: Fri, 14 Nov 2025 10:33:04 -0500 Subject: [PATCH 07/12] Add `isPositron`, semicolons --- apps/vscode/src/host/executors.ts | 3 ++ apps/vscode/src/providers/cell/commands.ts | 58 +++++++++++----------- 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/apps/vscode/src/host/executors.ts b/apps/vscode/src/host/executors.ts index fddfe2b5..8306cc87 100644 --- a/apps/vscode/src/host/executors.ts +++ b/apps/vscode/src/host/executors.ts @@ -32,6 +32,9 @@ export function executableLanguages() { return kCellExecutors.map((executor) => executor.language); } +// This function is always used by the `defaultExtensionHost`, and is used +// by the `hooksExtensionHost` as a backup. Please see `hooksExtensionHost` +// how executors are retrieved in Positron. export async function cellExecutorForLanguage( language: string, document: TextDocument, diff --git a/apps/vscode/src/providers/cell/commands.ts b/apps/vscode/src/providers/cell/commands.ts index 1531abea..e2250fdc 100644 --- a/apps/vscode/src/providers/cell/commands.ts +++ b/apps/vscode/src/providers/cell/commands.ts @@ -48,7 +48,7 @@ import { executeSelectionInteractive, } from "./executors"; import { ExtensionHost } from "../../host"; -import { hasHooks } from "../../host/hooks"; +import { tryAcquirePositronApi } from "@posit-dev/positron"; import { isKnitrDocument } from "../../host/executors"; import { commands } from "vscode"; import { virtualDocForCode, withVirtualDocUri } from "../../vdoc/vdoc"; @@ -56,6 +56,8 @@ import { embeddedLanguage } from "../../vdoc/languages"; import { Uri } from "vscode"; import { StatementRange } from "positron"; +const isPositron = tryAcquirePositronApi(); + export function cellCommands(host: ExtensionHost, engine: MarkdownEngine): Command[] { return [ new RunCurrentCommand(host, engine), @@ -150,9 +152,7 @@ abstract class RunCommand { } private async hasExecutorForLanguage(language: string, document: TextDocument, engine: MarkdownEngine) { - // TODO: this is incorrect right? `cellExecutorForLanguage` returns a promise, and a promise will always be truthy? - // We should have to await it before doing `!!` - return !!this.cellExecutorForLanguage(language, document, engine); + return undefined !== (await this.cellExecutorForLanguage(language, document, engine)); } } @@ -264,15 +264,15 @@ class RunPreviousCellCommand extends RunCommand implements Command { } // More permissive type than `Position` so its easier to construct via a literal -type LineAndCharPos = { line: number, character: number } +type LineAndCharPos = { line: number, character: number; }; // More permissive type than `Range` so its easier to construct via a literal -type LineAndCharRange = { start: LineAndCharPos, end: LineAndCharPos } +type LineAndCharRange = { start: LineAndCharPos, end: LineAndCharPos; }; function extractRangeFromCode(code: string, range: LineAndCharRange): string { - const extractedRange = lines(code).slice(range.start.line, range.end.line + 1) - extractedRange[0] = extractedRange[0].slice(range.start.character) - extractedRange[extractedRange.length - 1] = extractedRange[extractedRange.length - 1].slice(0, range.end.character) - return extractedRange.join('\n') + const extractedRange = lines(code).slice(range.start.line, range.end.line + 1); + extractedRange[0] = extractedRange[0].slice(range.start.character); + extractedRange[extractedRange.length - 1] = extractedRange[extractedRange.length - 1].slice(0, range.end.character); + return extractedRange.join('\n'); } // Run the code at the cursor @@ -308,7 +308,7 @@ class RunCurrentCommand extends RunCommand implements Command { const resolveToRunCell = editor.selection.isEmpty && !this.runSelection_ && !isKnitrDocument(editor.document, this.engine_) && - (!hasHooks() && (language === "python" || language === "r")); + (!isPositron && (language === "python" || language === "r")); if (resolveToRunCell) { const code = codeWithoutOptionsFromBlock(block); @@ -361,25 +361,25 @@ class RunCurrentCommand extends RunCommand implements Command { await executeInteractive(executor, [selection], editor.document); await editor.setBlockSelection(context, action); } - } + }; // if in Positron - if (hasHooks()) { + if (isPositron) { if (activeBlock && selection.length <= 0) { - const codeLines = lines(activeBlock.code) + const codeLines = lines(activeBlock.code); const vdoc = virtualDocForCode(codeLines, embeddedLanguage(activeBlock.language)!); if (vdoc) { const parentUri = Uri.file(editor.document.fileName); - const injectedLines = (vdoc.language?.inject?.length ?? 0) + const injectedLines = (vdoc.language?.inject?.length ?? 0); const positionIntoVdoc = (p: LineAndCharPos) => - new Position(p.line + injectedLines, p.character) + new Position(p.line + injectedLines, p.character); const positionOutOfVdoc = (p: LineAndCharPos) => - new Position(p.line - injectedLines, p.character) + new Position(p.line - injectedLines, p.character); const rangeOutOfVdoc = (r: Range): LineAndCharRange => ({ start: positionOutOfVdoc(r.start), end: positionOutOfVdoc(r.end) - }) + }); const getStatementRange = async (pos: LineAndCharPos) => { const result = await withVirtualDocUri(vdoc, parentUri, "statementRange", async (uri) => { return await commands.executeCommand( @@ -388,29 +388,29 @@ class RunCurrentCommand extends RunCommand implements Command { positionIntoVdoc(pos) ); }); - return rangeOutOfVdoc(result.range) - } + return rangeOutOfVdoc(result.range); + }; - const range = await getStatementRange(context.selection.start) - const code = extractRangeFromCode(activeBlock.code, range) + const range = await getStatementRange(context.selection.start); + const code = extractRangeFromCode(activeBlock.code, range); // BEGIN ref: https://github.com/posit-dev/positron/blob/main/src/vs/workbench/contrib/positronConsole/browser/positronConsoleActions.ts#L428 // strategy from Positron using `StatementRangeProvider` to find range of next statement // and move cursor based on that. if (range.end.line + 1 <= codeLines.length) { // get range of statement at line after current statement) - const nextRange = await getStatementRange(new Position(range.end.line + 1, 1)) + const nextRange = await getStatementRange(new Position(range.end.line + 1, 1)); if (nextRange.start.line > range.end.line) { - exec(nextRange.start, code) + exec(nextRange.start, code); // the next statement range may start before & end after the current statement if e.g. inside a function: } else if (nextRange.end.line > range.end.line) { - exec(nextRange.end, code) + exec(nextRange.end, code); } else { - exec("nextline", code) + exec("nextline", code); } } else { - exec("nextline", code) + exec("nextline", code); } // END ref. } @@ -428,9 +428,9 @@ class RunCurrentCommand extends RunCommand implements Command { } } else { if (selection.length > 0) { - exec("nextline", selection) + exec("nextline", selection); } else if (activeBlock) { // if the selection is empty take the whole line as the selection - exec("nextline", lines(activeBlock.code)[context.selection.start.line]) + exec("nextline", lines(activeBlock.code)[context.selection.start.line]); } } } From 34cd411826175482a45f1681ff9f51705fcce47e Mon Sep 17 00:00:00 2001 From: elliot Date: Fri, 14 Nov 2025 11:13:13 -0500 Subject: [PATCH 08/12] Fake code to call `positronConsole.executeCode` with uri, position returning position --- apps/vscode/src/host/executors.ts | 2 + apps/vscode/src/host/hooks.ts | 8 ++ apps/vscode/src/providers/cell/commands.ts | 82 +++++++-------------- apps/vscode/src/providers/cell/executors.ts | 8 ++ apps/vscode/src/vdoc/vdoc.ts | 5 +- 5 files changed, 48 insertions(+), 57 deletions(-) diff --git a/apps/vscode/src/host/executors.ts b/apps/vscode/src/host/executors.ts index 8306cc87..11db0d50 100644 --- a/apps/vscode/src/host/executors.ts +++ b/apps/vscode/src/host/executors.ts @@ -22,10 +22,12 @@ import { documentFrontMatter } from "../markdown/document"; import { isExecutableLanguageBlockOf } from "quarto-core"; import { workspace } from "vscode"; import { JupyterKernelspec } from "core"; +import { Position } from "vscode"; export interface CellExecutor { execute: (blocks: string[], editorUri?: Uri) => Promise; executeSelection?: () => Promise; + executeSelectionAtPosition?: (uri: Uri, pos: Position) => Promise; } export function executableLanguages() { diff --git a/apps/vscode/src/host/hooks.ts b/apps/vscode/src/host/hooks.ts index d7bbf093..1514c8e5 100644 --- a/apps/vscode/src/host/hooks.ts +++ b/apps/vscode/src/host/hooks.ts @@ -23,6 +23,8 @@ import { CellExecutor, cellExecutorForLanguage, executableLanguages, isKnitrDocu import { ExecuteQueue } from './execute-queue'; import { MarkdownEngine } from '../markdown/engine'; import { virtualDoc, adjustedPosition, unadjustedRange, withVirtualDocUri } from "../vdoc/vdoc"; +import { Position } from 'vscode'; +import { Uri } from 'vscode'; declare global { function acquirePositronApi(): hooks.PositronApi; @@ -83,6 +85,12 @@ export function hooksExtensionHost(): ExtensionHost { }, executeSelection: async (): Promise => { await vscode.commands.executeCommand('workbench.action.positronConsole.executeCode', { languageId: language }); + }, + executeSelectionAtPosition: async (uri: Uri, position: Position): Promise => { + return await vscode.commands.executeCommand( + 'workbench.action.positronConsole.executeCode', + { languageId: language, uri, position } + ); } }; diff --git a/apps/vscode/src/providers/cell/commands.ts b/apps/vscode/src/providers/cell/commands.ts index e2250fdc..bc7c384d 100644 --- a/apps/vscode/src/providers/cell/commands.ts +++ b/apps/vscode/src/providers/cell/commands.ts @@ -46,6 +46,7 @@ import { codeWithoutOptionsFromBlock, executeInteractive, executeSelectionInteractive, + executionSelectionAtPositionInteractive, } from "./executors"; import { ExtensionHost } from "../../host"; import { tryAcquirePositronApi } from "@posit-dev/positron"; @@ -265,15 +266,7 @@ class RunPreviousCellCommand extends RunCommand implements Command { // More permissive type than `Position` so its easier to construct via a literal type LineAndCharPos = { line: number, character: number; }; -// More permissive type than `Range` so its easier to construct via a literal -type LineAndCharRange = { start: LineAndCharPos, end: LineAndCharPos; }; - -function extractRangeFromCode(code: string, range: LineAndCharRange): string { - const extractedRange = lines(code).slice(range.start.line, range.end.line + 1); - extractedRange[0] = extractedRange[0].slice(range.start.character); - extractedRange[extractedRange.length - 1] = extractedRange[extractedRange.length - 1].slice(0, range.end.character); - return extractedRange.join('\n'); -} + // Run the code at the cursor class RunCurrentCommand extends RunCommand implements Command { @@ -355,14 +348,6 @@ class RunCurrentCommand extends RunCommand implements Command { const selection = context.selectedText; const activeBlock = context.blocks.find(block => block.active); - const exec = async (action: CodeViewSelectionAction, selection: string) => { - const executor = await this.cellExecutorForLanguage(context.activeLanguage, editor.document, this.engine_); - if (executor) { - await executeInteractive(executor, [selection], editor.document); - await editor.setBlockSelection(context, action); - } - }; - // if in Positron if (isPositron) { if (activeBlock && selection.length <= 0) { @@ -376,43 +361,23 @@ class RunCurrentCommand extends RunCommand implements Command { new Position(p.line + injectedLines, p.character); const positionOutOfVdoc = (p: LineAndCharPos) => new Position(p.line - injectedLines, p.character); - const rangeOutOfVdoc = (r: Range): LineAndCharRange => ({ - start: positionOutOfVdoc(r.start), - end: positionOutOfVdoc(r.end) - }); - const getStatementRange = async (pos: LineAndCharPos) => { - const result = await withVirtualDocUri(vdoc, parentUri, "statementRange", async (uri) => { - return await commands.executeCommand( - "vscode.executeStatementRangeProvider", + + const executor = await this.cellExecutorForLanguage(context.activeLanguage, editor.document, this.engine_); + if (executor) { + const nextStatementPos = await withVirtualDocUri( + vdoc, + parentUri, + "executeSelectionAtPositionInteractive", + (uri) => executionSelectionAtPositionInteractive( + executor, uri, - positionIntoVdoc(pos) - ); - }); - return rangeOutOfVdoc(result.range); - }; - - const range = await getStatementRange(context.selection.start); - const code = extractRangeFromCode(activeBlock.code, range); - - // BEGIN ref: https://github.com/posit-dev/positron/blob/main/src/vs/workbench/contrib/positronConsole/browser/positronConsoleActions.ts#L428 - // strategy from Positron using `StatementRangeProvider` to find range of next statement - // and move cursor based on that. - if (range.end.line + 1 <= codeLines.length) { - // get range of statement at line after current statement) - const nextRange = await getStatementRange(new Position(range.end.line + 1, 1)); - - if (nextRange.start.line > range.end.line) { - exec(nextRange.start, code); - // the next statement range may start before & end after the current statement if e.g. inside a function: - } else if (nextRange.end.line > range.end.line) { - exec(nextRange.end, code); - } else { - exec("nextline", code); + positionIntoVdoc(context.selection.start) + ) + ); + if (nextStatementPos !== undefined) { + await editor.setBlockSelection(context, positionOutOfVdoc(nextStatementPos)); } - } else { - exec("nextline", code); } - // END ref. } } // if not in Positron @@ -427,11 +392,18 @@ class RunCurrentCommand extends RunCommand implements Command { } } } else { - if (selection.length > 0) { - exec("nextline", selection); - } else if (activeBlock) { // if the selection is empty take the whole line as the selection - exec("nextline", lines(activeBlock.code)[context.selection.start.line]); + const executor = await this.cellExecutorForLanguage(context.activeLanguage, editor.document, this.engine_); + if (executor) { + if (selection.length > 0) { + await executeInteractive(executor, [selection], editor.document); + await editor.setBlockSelection(context, "nextline"); + } else if (activeBlock) { // if the selection is empty take the whole line as the selection + await executeInteractive(executor, [lines(activeBlock.code)[context.selection.start.line]], editor.document); + await editor.setBlockSelection(context, "nextline"); + } + } + } } } diff --git a/apps/vscode/src/providers/cell/executors.ts b/apps/vscode/src/providers/cell/executors.ts index e6092386..3581f1bb 100644 --- a/apps/vscode/src/providers/cell/executors.ts +++ b/apps/vscode/src/providers/cell/executors.ts @@ -34,6 +34,8 @@ import { cellOptionsForToken, kExecuteEval } from "./options"; import { CellExecutor, ExtensionHost } from "../../host"; import { executableLanguages } from "../../host/executors"; +import { Position } from "vscode"; +import { Uri } from "vscode"; export function hasExecutor(_host: ExtensionHost, language: string) { @@ -90,6 +92,12 @@ export async function executeInteractive( return await executor.execute(blocks, !document.isUntitled ? document.uri : undefined); } + +export async function executionSelectionAtPositionInteractive(executor: CellExecutor, uri: Uri, position: Position) { + if (executor?.executeSelectionAtPosition) { + return await executor.executeSelectionAtPosition(uri, position); + } +} // attempt language aware execution of current selection (returns false // if the executor doesn't support this, in which case generic // executeInteractive will be called) diff --git a/apps/vscode/src/vdoc/vdoc.ts b/apps/vscode/src/vdoc/vdoc.ts index f1441fe8..224b3506 100644 --- a/apps/vscode/src/vdoc/vdoc.ts +++ b/apps/vscode/src/vdoc/vdoc.ts @@ -118,9 +118,10 @@ export type VirtualDocAction = "definition" | "format" | "statementRange" | - "helpTopic"; + "helpTopic" | + "executeSelectionAtPositionInteractive"; -export type VirtualDocUri = { uri: Uri, cleanup?: () => Promise }; +export type VirtualDocUri = { uri: Uri, cleanup?: () => Promise; }; /** * Execute a callback on a virtual document's temporary URI From 361ca0eb584bcbc6f84c54cf4699d1d1bd0c9bdd Mon Sep 17 00:00:00 2001 From: elliot Date: Fri, 14 Nov 2025 11:46:07 -0500 Subject: [PATCH 09/12] Rename `executeSelectionAtPosition` to `executeAtPosition` --- apps/vscode/src/host/executors.ts | 2 +- apps/vscode/src/host/hooks.ts | 2 +- apps/vscode/src/providers/cell/commands.ts | 4 ++-- apps/vscode/src/providers/cell/executors.ts | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/vscode/src/host/executors.ts b/apps/vscode/src/host/executors.ts index 11db0d50..af23290e 100644 --- a/apps/vscode/src/host/executors.ts +++ b/apps/vscode/src/host/executors.ts @@ -27,7 +27,7 @@ import { Position } from "vscode"; export interface CellExecutor { execute: (blocks: string[], editorUri?: Uri) => Promise; executeSelection?: () => Promise; - executeSelectionAtPosition?: (uri: Uri, pos: Position) => Promise; + executeAtPosition?: (uri: Uri, pos: Position) => Promise; } export function executableLanguages() { diff --git a/apps/vscode/src/host/hooks.ts b/apps/vscode/src/host/hooks.ts index 1514c8e5..1db10456 100644 --- a/apps/vscode/src/host/hooks.ts +++ b/apps/vscode/src/host/hooks.ts @@ -86,7 +86,7 @@ export function hooksExtensionHost(): ExtensionHost { executeSelection: async (): Promise => { await vscode.commands.executeCommand('workbench.action.positronConsole.executeCode', { languageId: language }); }, - executeSelectionAtPosition: async (uri: Uri, position: Position): Promise => { + executeAtPosition: async (uri: Uri, position: Position): Promise => { return await vscode.commands.executeCommand( 'workbench.action.positronConsole.executeCode', { languageId: language, uri, position } diff --git a/apps/vscode/src/providers/cell/commands.ts b/apps/vscode/src/providers/cell/commands.ts index bc7c384d..d2829a35 100644 --- a/apps/vscode/src/providers/cell/commands.ts +++ b/apps/vscode/src/providers/cell/commands.ts @@ -46,7 +46,7 @@ import { codeWithoutOptionsFromBlock, executeInteractive, executeSelectionInteractive, - executionSelectionAtPositionInteractive, + executeAtPositionInteractive, } from "./executors"; import { ExtensionHost } from "../../host"; import { tryAcquirePositronApi } from "@posit-dev/positron"; @@ -368,7 +368,7 @@ class RunCurrentCommand extends RunCommand implements Command { vdoc, parentUri, "executeSelectionAtPositionInteractive", - (uri) => executionSelectionAtPositionInteractive( + (uri) => executeAtPositionInteractive( executor, uri, positionIntoVdoc(context.selection.start) diff --git a/apps/vscode/src/providers/cell/executors.ts b/apps/vscode/src/providers/cell/executors.ts index 3581f1bb..9f28fab9 100644 --- a/apps/vscode/src/providers/cell/executors.ts +++ b/apps/vscode/src/providers/cell/executors.ts @@ -93,9 +93,9 @@ export async function executeInteractive( } -export async function executionSelectionAtPositionInteractive(executor: CellExecutor, uri: Uri, position: Position) { - if (executor?.executeSelectionAtPosition) { - return await executor.executeSelectionAtPosition(uri, position); +export async function executeAtPositionInteractive(executor: CellExecutor, uri: Uri, position: Position) { + if (executor?.executeAtPosition) { + return await executor.executeAtPosition(uri, position); } } // attempt language aware execution of current selection (returns false From ba6bab6a8bb1d1d9e135f81aa19816506ea7a4bd Mon Sep 17 00:00:00 2001 From: elliot Date: Tue, 18 Nov 2025 16:04:35 -0500 Subject: [PATCH 10/12] modify command call to match @juliasilge's posit-dev/positron#10580 --- apps/vscode/src/host/hooks.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/vscode/src/host/hooks.ts b/apps/vscode/src/host/hooks.ts index 1db10456..b1e01ccc 100644 --- a/apps/vscode/src/host/hooks.ts +++ b/apps/vscode/src/host/hooks.ts @@ -88,8 +88,10 @@ export function hooksExtensionHost(): ExtensionHost { }, executeAtPosition: async (uri: Uri, position: Position): Promise => { return await vscode.commands.executeCommand( - 'workbench.action.positronConsole.executeCode', - { languageId: language, uri, position } + 'positron.executeCodeInConsole', + language, + uri, + position ); } }; From c2720b083bbb26de88f02751053e58f955b57fac Mon Sep 17 00:00:00 2001 From: elliot Date: Wed, 10 Dec 2025 17:16:19 -0500 Subject: [PATCH 11/12] Working VE statement execution with navigation to next statement --- apps/vscode/src/host/hooks.ts | 18 ++++++---- apps/vscode/src/providers/cell/commands.ts | 1 + packages/editor-types/src/codeview.ts | 5 +-- packages/editor/src/api/codeview.ts | 42 ++++++++++++++-------- packages/editor/src/api/navigation.ts | 17 +++++---- 5 files changed, 51 insertions(+), 32 deletions(-) diff --git a/apps/vscode/src/host/hooks.ts b/apps/vscode/src/host/hooks.ts index b1e01ccc..fc04f17d 100644 --- a/apps/vscode/src/host/hooks.ts +++ b/apps/vscode/src/host/hooks.ts @@ -87,12 +87,18 @@ export function hooksExtensionHost(): ExtensionHost { await vscode.commands.executeCommand('workbench.action.positronConsole.executeCode', { languageId: language }); }, executeAtPosition: async (uri: Uri, position: Position): Promise => { - return await vscode.commands.executeCommand( - 'positron.executeCodeInConsole', - language, - uri, - position - ); + try { + return await vscode.commands.executeCommand( + 'positron.executeCodeFromPosition', + language, + uri, + position + ) as Position; + } catch (e) { + // an error can happen, we think, if the statementRangeProvider errors + console.error('error when using `positron.executeCodeFromPosition`'); + } + return position; } }; diff --git a/apps/vscode/src/providers/cell/commands.ts b/apps/vscode/src/providers/cell/commands.ts index d2829a35..0a042d68 100644 --- a/apps/vscode/src/providers/cell/commands.ts +++ b/apps/vscode/src/providers/cell/commands.ts @@ -374,6 +374,7 @@ class RunCurrentCommand extends RunCommand implements Command { positionIntoVdoc(context.selection.start) ) ); + if (nextStatementPos !== undefined) { await editor.setBlockSelection(context, positionOutOfVdoc(nextStatementPos)); } diff --git a/packages/editor-types/src/codeview.ts b/packages/editor-types/src/codeview.ts index e40da3ba..580d0a93 100644 --- a/packages/editor-types/src/codeview.ts +++ b/packages/editor-types/src/codeview.ts @@ -27,14 +27,15 @@ export const kCodeViewGetDiagnostics = 'code_view_get_diagnostics'; export type CodeViewExecute = "selection" | "cell" | "cell+advance" | "above" | "below"; +export type CodeViewBlock = { pos: number, language: string, code: string; active: boolean; }; export interface CodeViewActiveBlockContext { activeLanguage: string; - blocks: Array<{ pos: number, language: string, code: string; active: boolean; }>; + blocks: Array; selection: Range; selectedText: string; } -export type CodeViewSelectionAction = "nextline" | "nextblock" | "prevblock" | { line: number, character: number }; +export type CodeViewSelectionAction = "nextline" | "nextblock" | "prevblock" | { line: number, character: number; }; export interface CodeViewCellContext { filepath: string; diff --git a/packages/editor/src/api/codeview.ts b/packages/editor/src/api/codeview.ts index 3f664d11..4b3f88e3 100644 --- a/packages/editor/src/api/codeview.ts +++ b/packages/editor/src/api/codeview.ts @@ -31,8 +31,8 @@ import { editingRootNode } from './node'; import { editorScrollContainer } from './scroll'; import { rmdChunk } from './rmd'; -import { CodeViewActiveBlockContext, CodeViewCellContext, CodeViewCompletionContext, CodeViewSelectionAction } from 'editor-types'; -import { navigateToPos } from './navigation'; +import { CodeViewActiveBlockContext, CodeViewBlock, CodeViewCellContext, CodeViewCompletionContext, CodeViewSelectionAction } from 'editor-types'; +import { navigateToPos, setSelection } from './navigation'; export const kCodeViewNextLineTransaction = "codeViewNextLine"; @@ -200,6 +200,28 @@ export function scrollCodeViewElementIntoView(ele: HTMLElement, codeViewDom: HTM } } +// convert action line and character in code block space to pos in prosemirror space +function codeBlockPositionToViewPos(block: CodeViewBlock, action: { line: number, character: number; }) { + // asummes the meta line looks like this: + const metaLine = '{' + block.language + '}\n'; + // block.code always has a trailing newline, resulting in an empty line here that is not in the actual editor + // so we slice it off + const code = lines(block.code).slice(0, -1); + + if (action.line >= code.length) { + const endOfBlockPos = block.pos + metaLine.length + block.code.length - 1; + return endOfBlockPos; + } + + let pos = block.pos + metaLine.length; + for (let i = 0; i < action.line; i++) { + pos += code[i].length + 1; + } + pos += action.character; + + return pos; +} + export function codeViewSetBlockSelection( view: EditorView, context: CodeViewActiveBlockContext, @@ -209,19 +231,9 @@ export function codeViewSetBlockSelection( if (activeIndex !== -1) { if (typeof action === 'object') { - // convert action line and character in code block space to pos in prosemirror space - const block = context.blocks[activeIndex] - // asummes the meta line looks like this: - const metaLine = '{' + block.language + '}\n' - const code = lines(block.code) - if (action.line > code.length) throw 'trying to move cursor outside block!' - let pos = block.pos + metaLine.length - for (let i = 0; i < action.line; i++) { - pos += code[i].length + 1 - } - pos += action.character - - navigateToPos(view, pos, false) + // action is of type `{ line: number, character: number }` + view.focus(); + setSelection(view, codeBlockPositionToViewPos(context.blocks[activeIndex], action)); } else if (action === "nextline") { const tr = view.state.tr; diff --git a/packages/editor/src/api/navigation.ts b/packages/editor/src/api/navigation.ts index b7eddcb7..9825c4ff 100644 --- a/packages/editor/src/api/navigation.ts +++ b/packages/editor/src/api/navigation.ts @@ -81,14 +81,7 @@ export function navigateToXRef(view: EditorView, editorFormat: EditorFormat, xre } } -export function navigateToPos(view: EditorView, pos: number, animate = true): Navigation | null { - // get previous position - const prevPos = view.state.selection.from; - - // need to target at least the body - pos = Math.max(pos, 2); - - // set selection (detect node selection) +export function setSelection(view: EditorView, pos: number) { const tr = view.state.tr; const pmNode = view.state.doc.nodeAt(pos); if (pmNode?.type.spec.selectable) { @@ -98,6 +91,12 @@ export function navigateToPos(view: EditorView, pos: number, animate = true): Na } tr.setMeta(kNavigationTransaction, true); view.dispatch(tr); +} +export function navigateToPos(view: EditorView, pos: number, animate = true): Navigation | null { + // need to target at least the body + pos = Math.max(pos, 2); + + setSelection(view, pos); // find a targetable dom node at the position const node = findDomRefAtPos(pos, view.domAtPos.bind(view)); @@ -132,7 +131,7 @@ export function navigateToPos(view: EditorView, pos: number, animate = true): Na } }, 200); - return { pos, prevPos }; + return { pos, prevPos: view.state.selection.from }; } else { return null; } From f775162cd76e7f9a78098624c0569d2ce137e36e Mon Sep 17 00:00:00 2001 From: elliot Date: Wed, 10 Dec 2025 17:17:52 -0500 Subject: [PATCH 12/12] bump required Positron version --- apps/vscode/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/vscode/package.json b/apps/vscode/package.json index 906214ac..c6aff2f7 100644 --- a/apps/vscode/package.json +++ b/apps/vscode/package.json @@ -29,7 +29,7 @@ "private": true, "engines": { "vscode": "^1.75.0", - "positron": "^2025.6.0" + "positron": "^2025.12.0" }, "main": "./out/main.js", "browser": "./browser.js",