Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ jobs:
run: |
npm run build:sidecar
npm run stage:sidecar
- name: Real R smoke test
- name: Real R runtime and frontend submission tests
run: npm run test:runtime
- name: Package and verify VSIX
run: |
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to R Console will be documented in this file.

## [Unreleased]

### Fixed

- Improved the reliability of code submission in the R Console.

### Security

- Improved the security of R Console connections, including persistent sessions and language-server communication.
Expand Down
27 changes: 27 additions & 0 deletions docs/IMPLEMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,33 @@ Creating a console follows this implementation path:
8. backend events update terminal state
9. the extension schedules persistence of the session record

Enter, accepted multiline paste, and programmatic sends reserve their submission
positions synchronously. Completeness checks run in reservation order, while
completed expressions can enter execution immediately. An incomplete tail passes
to the next reservation before that reservation is checked, including when the
tail is waiting behind an executing expression. With no accepted successor, the
tail becomes editable; normal input handling includes that buffer in the next
Enter or paste snapshot. It is never owned by both the queue and editor.
Enter retains literal whitespace and applies its continuation newline at the
saved cursor only when input is incomplete. Each complete expression is recorded
in history as it is accepted, so interrupting later parsing preserves earlier
entries.

This ordering retains the existing TypeScript heuristic and optional native R
parse-status check. Native checks wait for a confirmed top-level ready state
before sending; their 150 ms timeout starts only after sending. A lazy per-console
notification wakes waiting checks on readiness or cancellation, without polling.
Each submission passes its own console's native parser explicitly; the language
parser holds no shared session callback. Connecting, interrupting, or closing one
console cannot change another console's parser or readiness wait. Runtime callbacks
and socket data from a detached or replaced session cannot update its successor.
Dispatch also requires readiness, including after attaching to an already busy R
session. Cancellation invalidates old reservations by generation; fresh
input does not wait for a cancelled parser. No speculative checks or collection
of previously checked prefixes is retained. Failed delivery restores pending
source using the same continuation boundaries, preserving existing newlines and
saved editing positions instead of adding separators unconditionally.

### 4.5 Attaching A Detached Console

Attaching a detached console follows this implementation path:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@
"scripts": {
"test": "node --test tests/unit/*.test.cjs",
"test:extension-host": "vscode-test",
"test:runtime": "node --test tests/runtime.test.cjs",
"test:runtime": "node --test tests/runtime.test.cjs tests/submission-runtime.test.cjs",
"test:package": "node --test tests/package.test.cjs",
"vscode:prepublish": "npm run package:extension && npm run build:sidecar && npm run stage:sidecar",
"build:sidecar": "node scripts/build-sidecar.js",
Expand Down
19 changes: 7 additions & 12 deletions src/Language/parser.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
let nativeParseCallback: ((code: string) => Promise<number>) | null = null;

export function setNativeParseCallback(
callback: ((code: string) => Promise<number>) | null
): void {
nativeParseCallback = callback;
}
export type NativeParseCallback = (code: string) => Promise<number>;

type LocalParseClassification = "complete" | "incomplete" | "unknown";

Expand Down Expand Up @@ -606,13 +600,14 @@ interface ParserState {
bracketDepth: number;
}

export async function isExpressionCompleteAsync(code: string): Promise<boolean> {
const result = await getExpressionCompletenessAsync(code);
export async function isExpressionCompleteAsync(code: string, nativeParse?: NativeParseCallback): Promise<boolean> {
const result = await getExpressionCompletenessAsync(code, nativeParse);
return result.isComplete;
}

async function getExpressionCompletenessAsync(
code: string
code: string,
nativeParse?: NativeParseCallback
): Promise<{ isComplete: boolean; localClassification: LocalParseClassification }> {
const sanitized = stripCommentLines(code);
const trimmed = sanitized.trim();
Expand All @@ -625,9 +620,9 @@ async function getExpressionCompletenessAsync(
return { isComplete: false, localClassification };
}

if (nativeParseCallback) {
if (nativeParse) {
try {
const status = await nativeParseCallback(sanitized);
const status = await nativeParse(sanitized);
return { isComplete: status !== 2, localClassification };
} catch {
}
Expand Down
6 changes: 5 additions & 1 deletion src/Runtime/runtimeBackend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,8 @@ export class RustSidecarRuntimeBackend implements RuntimeBackend {
return;
}
state.pendingParseRequests.delete(requestId);
settled = true;
// reject() owns the settled flag; setting it here would leave the
// promise pending forever and prevent the parser's local fallback.
pending.reject(new Error("native parse-status request timed out"));
}, PARSE_STATUS_TIMEOUT_MS);
state.pendingParseRequests.set(requestId, {
Expand Down Expand Up @@ -496,6 +497,9 @@ export class RustSidecarRuntimeBackend implements RuntimeBackend {
state.socketCarry = Buffer.alloc(0);

socket.on("data", (chunk: Buffer) => {
if (state.socket !== socket || state.attachGeneration !== generation) {
return;
}
const parsed = parseBackendFrames(chunk, state.socketCarry);
state.socketCarry = parsed.carry;

Expand Down
5 changes: 3 additions & 2 deletions src/Terminal/inputState.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
isExpressionCompleteAsync as parserIsExpressionCompleteAsync,
type NativeParseCallback,
} from "../Language/parser";
import {
moveCursorByRenderedRows,
Expand Down Expand Up @@ -185,8 +186,8 @@ export class InputState {
this.preferredColumn = null;
}

async isExpressionCompleteAsync(code: string = this._text): Promise<boolean> {
return parserIsExpressionCompleteAsync(code);
async isExpressionCompleteAsync(code: string = this._text, nativeParse?: NativeParseCallback): Promise<boolean> {
return parserIsExpressionCompleteAsync(code, nativeParse);
}

private moveByRenderedRows(
Expand Down
Loading
Loading