Skip to content
Closed
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 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/raw-string-r.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
96 changes: 21 additions & 75 deletions src/Language/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ interface Token {
type: TokenType;
value: string;
position: number;
closed?: boolean;
}

export type HighlightTokenKind =
Expand Down Expand Up @@ -460,55 +461,29 @@ function parseRawString(
pos: number
): { token: Token; newPos: number } | null {
const start = pos;
pos += 1;

const quote = code[pos];
pos += 1;

let delimiter = "";
if (pos < code.length) {
const delim = code[pos];
if (delim === "(" || delim === "[" || delim === "{") {
delimiter = delim;
pos += 1;
} else if (delim === "-") {
let dashes = "";
while (pos < code.length && code[pos] === "-") {
dashes += "-";
pos += 1;
}
delimiter = dashes;
}
}

const closeDelim =
delimiter === "(" ? ")" : delimiter === "[" ? "]" : delimiter === "{" ? "}" : delimiter;

while (pos < code.length) {
if (delimiter) {
if (code[pos] === closeDelim) {
const remaining = code.slice(pos);
if (remaining.startsWith(closeDelim + quote)) {
pos += closeDelim.length + 1;
return {
token: { type: TokenType.RawString, value: code.slice(start, pos), position: start },
newPos: pos,
};
}
}
} else if (code[pos] === quote) {
pos += 1;
return {
token: { type: TokenType.RawString, value: code.slice(start, pos), position: start },
newPos: pos,
};
}
const quote = code[pos + 1];
pos += 2;
const dashStart = pos;
while (code[pos] === "-") {
pos += 1;
}
const closingBracket =
code[pos] === "(" ? ")" : code[pos] === "[" ? "]" : code[pos] === "{" ? "}" : undefined;
if (!closingBracket) {
return null;
}

const terminator = closingBracket + code.slice(dashStart, pos) + quote;
const end = code.indexOf(terminator, pos + 1);
const newPos = end < 0 ? code.length : end + terminator.length;
return {
token: { type: TokenType.RawString, value: code.slice(start, pos), position: start },
newPos: pos,
token: {
type: TokenType.RawString,
value: code.slice(start, newPos),
position: start,
closed: end >= 0,
},
newPos,
};
}

Expand Down Expand Up @@ -661,7 +636,7 @@ function classifyExpressionHeuristic(code: string): LocalParseClassification {
break;
}
case TokenType.RawString:
if (!isRawStringClosed(token.value)) {
if (!token.closed) {
return "incomplete";
}
break;
Expand Down Expand Up @@ -794,35 +769,6 @@ function isDefinitelyCompleteToken(token: Token): boolean {
}
}

function isRawStringClosed(value: string): boolean {
if (value.length < 3) {
return false;
}

const quote = value[1];
if (value.length >= 4) {
const delim = value[2];
if (delim === "(") {
return value.endsWith(`)${quote}`);
}
if (delim === "[") {
return value.endsWith(`]${quote}`);
}
if (delim === "{") {
return value.endsWith(`}${quote}`);
}
if (delim === "-") {
let dashes = 0;
for (let i = 2; i < value.length && value[i] === "-"; i += 1) {
dashes += 1;
}
return value.endsWith(`${"-".repeat(dashes)}${quote}`);
}
}

return value.endsWith(quote);
}

function isAwaitingBody(tokens: Token[]): boolean {
let depth = 0;
let parenStart = -1;
Expand Down
149 changes: 101 additions & 48 deletions src/Terminal/rTerminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,8 +221,10 @@ export class RTerminal implements vscode.Pseudoterminal {
private runtimeHostAdapter!: RuntimeHost;

private programmaticSubmissionQueue: Promise<void> = Promise.resolve();
private inputCommitPending = false;
private suppressNextEnterAfterPasteEnd = false;

private submissionGeneration = 0;
private submissionQueue: Submission[] = [];
private activeSubmission: Submission | null = null;
private extensionResourcesDisposePromise: Promise<void> | undefined;
Expand Down Expand Up @@ -525,6 +527,10 @@ export class RTerminal implements vscode.Pseudoterminal {
set activeSubmission(value) {
self.activeSubmission = value;
},
get submissionGeneration() {
return self.submissionGeneration;
},
cancelPendingSubmissions: () => self.cancelPendingSubmissions(),
get submissionQueue() {
return self.submissionQueue;
},
Expand Down Expand Up @@ -869,8 +875,7 @@ export class RTerminal implements vscode.Pseudoterminal {
!this.inBracketPaste &&
this.mode !== "closed" &&
this.mode !== "reply" &&
this.inputState.text.length === 0 &&
this.inputState.isAtEnd
(this.inputCommitPending || (this.inputState.text.length === 0 && this.inputState.isAtEnd))
);
}

Expand Down Expand Up @@ -924,7 +929,7 @@ export class RTerminal implements vscode.Pseudoterminal {
return;
}

if (this.mode !== "ready" || !this.promptReady) {
if (this.mode !== "ready" || !this.promptReady || this.inputCommitPending) {
this.pendingProgrammaticInput = data;
this.clearPendingInputFlushTimer();
this.pendingInputFlushTimer = setTimeout(() => {
Expand All @@ -950,26 +955,23 @@ export class RTerminal implements vscode.Pseudoterminal {
}

private queueProgrammaticSubmission(data: string): void {
const generation = this.submissionGeneration;
this.programmaticSubmissionQueue = this.programmaticSubmissionQueue.then(
async () => this.handleQueuedProgrammaticSubmission(data),
async () => this.handleQueuedProgrammaticSubmission(data)
async () => this.handleQueuedProgrammaticSubmission(data, generation),
async () => this.handleQueuedProgrammaticSubmission(data, generation)
);
}

private async handleQueuedProgrammaticSubmission(data: string): Promise<void> {
if (this.mode === "closed") {
return;
}

if (!this.canRunProgrammaticSubmission()) {
private async handleQueuedProgrammaticSubmission(data: string, generation: number): Promise<void> {
while (this.mode !== "closed" && generation === this.submissionGeneration) {
if (this.canRunProgrammaticSubmission()) {
await this.handleProgrammaticSubmission(data);
return;
}
await new Promise<void>((resolve) => {
setTimeout(resolve, this.getProgrammaticSubmissionFlushDelay());
});
await this.handleQueuedProgrammaticSubmission(data);
return;
}

await this.handleProgrammaticSubmission(data);
}

private canRunProgrammaticSubmission(): boolean {
Expand All @@ -985,9 +987,8 @@ export class RTerminal implements vscode.Pseudoterminal {
const normalized = stripBracketedPasteMarkers(data)
.replace(/\r\n/g, "\n")
.replace(/\r/g, "\n");
const trimmed = normalized.replace(/\n+$/, "");
const submission = trimmed.trimEnd();
if (!submission) {
const submission = this.stripFinalInputNewline(normalized);
if (!submission.trim()) {
return;
}

Expand All @@ -998,22 +999,7 @@ export class RTerminal implements vscode.Pseudoterminal {
this.rHistory.resetIndex();

const blocks = await this.enqueueRSubmission(submission, false);
if (blocks.length > 0) {
this.recordSubmissionHistory(blocks);
return;
}

const isComplete = await this.inputState.isExpressionCompleteAsync(submission);
if (
!isComplete &&
this.mode === "ready" &&
this.promptReady &&
this.inputState.text.length === 0 &&
this.inputState.isAtEnd
) {
this.inputState.insertText(trimmed);
this.renderInput();
}
this.recordSubmissionHistory(blocks);
}

private handleNestedReplyPaste(content: string): boolean {
Expand Down Expand Up @@ -1111,6 +1097,11 @@ export class RTerminal implements vscode.Pseudoterminal {
if (this.mode === "closed") {
return;
}
// Committed Enter/paste input is immutable during the existing parse.
// Ctrl+C still cancels the pending submission.
if (this.inputCommitPending && action.type !== "ctrl_c") {
return;
}

if (this.mode === "executing" || this.mode === "starting") {
if (action.type === "ctrl_c") {
Expand Down Expand Up @@ -1396,6 +1387,7 @@ export class RTerminal implements vscode.Pseudoterminal {
}
return;
case "ctrl_c":
this.cancelPendingSubmissions();
if (this.inputState.text.length > 0) {
this.clearInputRender();
this.inputState.reset();
Expand Down Expand Up @@ -1578,27 +1570,52 @@ export class RTerminal implements vscode.Pseudoterminal {
return;
}

const cursorAtEnd = this.inputState.text.length === 0 || this.inputState.isAtEnd;
const trimmed = normalized.replace(/\n+$/, "");
const fullText = this.inputState.text + trimmed;
// Apply the paste before awaiting the parser, preserving input order.
// Avoid rendering before a complete paste is submitted to R.
const cursorAtEnd = this.inputState.isAtEnd;
this.inputState.insertText(normalized.replace(/\n+$/, ""));
if (!cursorAtEnd) {
this.renderInput();
return;
}

const isComplete = await this.inputState.isExpressionCompleteAsync(fullText);
const fullText = this.inputState.text;
const generation = this.submissionGeneration;
this.inputCommitPending = true;
let isComplete: boolean;
try {
isComplete = await this.inputState.isExpressionCompleteAsync(fullText);
} finally {
if (generation === this.submissionGeneration) {
this.inputCommitPending = false;
}
}
if (generation !== this.submissionGeneration || this.mode !== "ready") {
return;
}

if ((endsWithNewline || containsNewline) && cursorAtEnd && isComplete) {
if (isComplete) {
const submission = fullText.trimEnd();
if (submission) {
this.inputState.reset();
this.historyBrowsing = false;
this.savedHistoryInput = undefined;
this.historyCollapsed = true;
this.rHistory.resetIndex();
const blocks = await this.enqueueRSubmission(submission, !/[\r\n]/.test(submission));
this.recordSubmissionHistory(blocks);
// The committed input stays owned until async block splitting has queued it.
this.inputCommitPending = true;
try {
const blocks = await this.enqueueRSubmission(submission, !/[\r\n]/.test(submission));
this.recordSubmissionHistory(blocks);
} finally {
if (generation === this.submissionGeneration) {
this.inputCommitPending = false;
}
}
}
return;
}

this.inputState.insertText(trimmed);
this.renderInput();
}

Expand Down Expand Up @@ -1688,7 +1705,20 @@ export class RTerminal implements vscode.Pseudoterminal {
return;
}

const isComplete = await this.inputState.isExpressionCompleteAsync();
const generation = this.submissionGeneration;
const checkedText = this.inputState.text;
this.inputCommitPending = true;
let isComplete: boolean;
try {
isComplete = await this.inputState.isExpressionCompleteAsync(checkedText);
} finally {
if (generation === this.submissionGeneration) {
this.inputCommitPending = false;
}
}
if (generation !== this.submissionGeneration || this.mode !== "ready") {
return;
}
if (!isComplete) {
if (this.inputState.cursorCol === 0) {
this.inputState.insertText("\n");
Expand Down Expand Up @@ -1747,11 +1777,19 @@ export class RTerminal implements vscode.Pseudoterminal {
return;
}

const blocks = await this.enqueueRSubmission(
sanitized,
!/[\r\n]/.test(sanitized)
);
this.recordSubmissionHistory(blocks);
// Keep ownership through block splitting, not only the first parse.
this.inputCommitPending = true;
try {
const blocks = await this.enqueueRSubmission(
sanitized,
!/[\r\n]/.test(sanitized)
);
this.recordSubmissionHistory(blocks);
} finally {
if (generation === this.submissionGeneration) {
this.inputCommitPending = false;
}
}
}

private reverseSearch(): void {
Expand Down Expand Up @@ -2222,6 +2260,21 @@ export class RTerminal implements vscode.Pseudoterminal {
sendRuntimeReply(this.runtimeHost(), text);
}

private cancelPendingSubmissions(): void {
this.submissionGeneration += 1;
this.submissionQueue = [];
this.programmaticSubmissionQueue = Promise.resolve();
this.inputCommitPending = false;
this.pendingProgrammaticInput = "";
this.clearPendingInputFlushTimer();
this.clearPendingConsoleInput();
this.pasteBuffer = "";
this.inBracketPaste = false;
this.suppressNextEnterAfterPasteEnd = false;
this.submissionPending = false;
this.awaitingExecutionStart = false;
}

private interruptR(): void {
interruptRuntime(this.runtimeHost());
}
Expand Down
Loading
Loading