From 2dede3f9306d8a91913852d35bcfba170d270b80 Mon Sep 17 00:00:00 2001 From: elliot Date: Wed, 24 Sep 2025 15:53:45 -0400 Subject: [PATCH 1/4] 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 a95b1d80..077a2106 100644 --- a/apps/vscode/src/providers/cell/commands.ts +++ b/apps/vscode/src/providers/cell/commands.ts @@ -49,6 +49,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 [ @@ -345,17 +349,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 c58152fa0f3dffcdd041eda7eae659aab6184471 Mon Sep 17 00:00:00 2001 From: elliot Date: Fri, 3 Oct 2025 15:31:25 -0400 Subject: [PATCH 2/4] 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 077a2106..524aa583 100644 --- a/apps/vscode/src/providers/cell/commands.ts +++ b/apps/vscode/src/providers/cell/commands.ts @@ -355,10 +355,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", @@ -373,6 +373,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 + } + } } } @@ -389,8 +412,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 349a961a..07fa1325 100644 --- a/packages/editor-types/src/codeview.ts +++ b/packages/editor-types/src/codeview.ts @@ -31,7 +31,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 7f35b6377dece8499debfae74a11dd85454db34c Mon Sep 17 00:00:00 2001 From: elliot Date: Fri, 3 Oct 2025 16:45:17 -0400 Subject: [PATCH 3/4] 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 524aa583..02f55c00 100644 --- a/apps/vscode/src/providers/cell/commands.ts +++ b/apps/vscode/src/providers/cell/commands.ts @@ -374,28 +374,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 a0d5086bfe6f0c3a62b8e5230c148b9b7ea15ae6 Mon Sep 17 00:00:00 2001 From: elliot Date: Tue, 21 Oct 2025 10:50:33 -0400 Subject: [PATCH 4/4] 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 02f55c00..ce74ccff 100644 --- a/apps/vscode/src/providers/cell/commands.ts +++ b/apps/vscode/src/providers/cell/commands.ts @@ -340,8 +340,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) { @@ -359,37 +362,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); } }