Refactor DocumentRange to allow multiline ranges
This commit is contained in:
parent
0a3bac3517
commit
a05731e91d
@ -117,13 +117,13 @@ export default class Befunge93LanguageEngine
|
||||
const lines = code.split("\n");
|
||||
if (lines.length > COLSIZE)
|
||||
throw new ParseError(`Code is longer than ${COLSIZE} lines`, {
|
||||
line: COLSIZE,
|
||||
startLine: COLSIZE,
|
||||
});
|
||||
lines.forEach((line, idx) => {
|
||||
if (line.length > ROWSIZE)
|
||||
throw new ParseError(`Line is longer than ${ROWSIZE} characters`, {
|
||||
line: idx,
|
||||
charRange: { start: ROWSIZE },
|
||||
startLine: idx,
|
||||
startCol: ROWSIZE,
|
||||
});
|
||||
});
|
||||
|
||||
@ -359,7 +359,7 @@ export default class Befunge93LanguageEngine
|
||||
// Return code edit object
|
||||
return {
|
||||
text: toSafePrintableChar(asciiVal),
|
||||
range: { line: y, charRange: { start: x, end: x + 1 } },
|
||||
range: { startLine: y, startCol: x, endCol: x + 1 },
|
||||
};
|
||||
}
|
||||
|
||||
@ -418,15 +418,16 @@ export default class Befunge93LanguageEngine
|
||||
// Add padding to line upto full-length
|
||||
edits.push({
|
||||
range: {
|
||||
line: i,
|
||||
charRange: { start: lines[i].length, end: lines[i].length },
|
||||
startLine: i,
|
||||
startCol: lines[i].length,
|
||||
endCol: lines[i].length,
|
||||
},
|
||||
text: " ".repeat(ROWSIZE - lines[i].length),
|
||||
});
|
||||
} else {
|
||||
// Add full-length empty line
|
||||
edits.push({
|
||||
range: { line: i, charRange: { start: 0, end: 0 } },
|
||||
range: { startLine: i, startCol: 0, endCol: 0 },
|
||||
text: "\n" + " ".repeat(80),
|
||||
});
|
||||
}
|
||||
@ -448,7 +449,7 @@ export default class Befunge93LanguageEngine
|
||||
|
||||
/** Convert 2D coordinates to DocumentRange */
|
||||
private toRange(line: number, char: number): DocumentRange {
|
||||
return { line, charRange: { start: char, end: char + 1 } };
|
||||
return { startLine: line, startCol: char, endCol: char + 1 };
|
||||
}
|
||||
|
||||
/** Check if given coordinates lies inside 80x25 grid */
|
||||
|
@ -51,8 +51,7 @@ export default class BrainfuckLanguageEngine implements LanguageEngine<BFRS> {
|
||||
let nextStepLocation: DocumentRange | null = null;
|
||||
if (this._pc < this._ast.length) {
|
||||
const { line, char } = this._ast[this._pc].location;
|
||||
const charRange = { start: char, end: char + 1 };
|
||||
nextStepLocation = { line, charRange };
|
||||
nextStepLocation = { startLine: line, startCol: char, endCol: char + 1 };
|
||||
}
|
||||
|
||||
// Prepare and return execution result
|
||||
@ -85,8 +84,9 @@ export default class BrainfuckLanguageEngine implements LanguageEngine<BFRS> {
|
||||
jumpTarget = loopStack.pop();
|
||||
if (jumpTarget == null)
|
||||
throw new ParseError("Unmatched ']'", {
|
||||
line: lIdx,
|
||||
charRange: { start: cIdx, end: cIdx + 1 },
|
||||
startLine: lIdx,
|
||||
startCol: cIdx,
|
||||
endCol: cIdx + 1,
|
||||
});
|
||||
// Add closing end location to loop-opener
|
||||
ast[jumpTarget].instr.param = ast.length;
|
||||
@ -105,8 +105,9 @@ export default class BrainfuckLanguageEngine implements LanguageEngine<BFRS> {
|
||||
const opener = loopStack[loopStack.length - 1];
|
||||
const location = ast[opener].location;
|
||||
throw new ParseError("Unmatched '['", {
|
||||
line: location.line,
|
||||
charRange: { start: location.char, end: location.char + 1 },
|
||||
startLine: location.line,
|
||||
startCol: location.char,
|
||||
endCol: location.char + 1,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -30,8 +30,9 @@ export const parseProgram = (code: string): T.ChefProgram => {
|
||||
// Location of code's last char, used for errors
|
||||
const lastCharPosition = stack[0]?.line.length - 1 || 0;
|
||||
const lastCharRange: DocumentRange = {
|
||||
line: stack.length - 1,
|
||||
charRange: { start: lastCharPosition, end: lastCharPosition + 1 },
|
||||
startLine: stack.length - 1,
|
||||
startCol: lastCharPosition,
|
||||
endCol: lastCharPosition + 1,
|
||||
};
|
||||
|
||||
// Exhaust any empty lines at the start of the program
|
||||
@ -69,9 +70,11 @@ const parseTitle = (stack: CodeStack, lastCharRange: DocumentRange): string => {
|
||||
const { line, row } = popCodeStack(stack, true);
|
||||
if (line === null)
|
||||
throw new ParseError("Expected recipe title", lastCharRange);
|
||||
if (!line) throw new ParseError("Expected recipe title", { line: row });
|
||||
if (!line) throw new ParseError("Expected recipe title", { startLine: row });
|
||||
if (!line.endsWith("."))
|
||||
throw new ParseError("Recipe title must end with period", { line: row });
|
||||
throw new ParseError("Recipe title must end with period", {
|
||||
startLine: row,
|
||||
});
|
||||
return line.slice(0, -1);
|
||||
};
|
||||
|
||||
@ -86,7 +89,7 @@ const parseEmptyLine = (
|
||||
): void => {
|
||||
const { line, row } = popCodeStack(stack, true);
|
||||
if (line === null) throw new ParseError("Expected blank line", lastCharRange);
|
||||
if (line) throw new ParseError("Expected blank line", { line: row });
|
||||
if (line) throw new ParseError("Expected blank line", { startLine: row });
|
||||
};
|
||||
|
||||
/** Parse the stack for method instructions section */
|
||||
@ -98,7 +101,7 @@ const parseRecipeComments = (stack: CodeStack): void => {
|
||||
const parseIngredientsHeader = (stack: CodeStack): void => {
|
||||
const { line, row } = popCodeStack(stack, true);
|
||||
if (line !== "Ingredients.")
|
||||
throw new ParseError("Expected ingredients header", { line: row });
|
||||
throw new ParseError("Expected ingredients header", { startLine: row });
|
||||
};
|
||||
|
||||
/** Parse the stack for ingredient definition lines */
|
||||
@ -117,7 +120,7 @@ const parseCookingTime = (stack: CodeStack): void => {
|
||||
const regex = /^Cooking time: \d+ (?:hours?|minutes?).$/;
|
||||
const { line, row } = popCodeStack(stack, true);
|
||||
if (!line!.match(regex))
|
||||
throw new ParseError("Invalid cooking time statement", { line: row });
|
||||
throw new ParseError("Invalid cooking time statement", { startLine: row });
|
||||
};
|
||||
|
||||
/** Parse stack for oven setting statement. No data is returned. */
|
||||
@ -126,14 +129,14 @@ const parseOvenSetting = (stack: CodeStack): void => {
|
||||
/^Pre-heat oven to \d+ degrees Celsius(?: \(gas mark [\d/]+\))?.$/;
|
||||
const { line, row } = popCodeStack(stack, true);
|
||||
if (!line!.match(regex))
|
||||
throw new ParseError("Invalid oven setting statement", { line: row });
|
||||
throw new ParseError("Invalid oven setting statement", { startLine: row });
|
||||
};
|
||||
|
||||
/** Parse the stack for the header of method section */
|
||||
const parseMethodHeader = (stack: CodeStack): void => {
|
||||
const { line, row } = popCodeStack(stack, true);
|
||||
if (line !== "Method.")
|
||||
throw new ParseError('Expected "Method."', { line: row });
|
||||
throw new ParseError('Expected "Method."', { startLine: row });
|
||||
};
|
||||
|
||||
/** Parse the stack for method instructions section */
|
||||
@ -233,7 +236,7 @@ const serializeMethodOps = (stack: CodeStack): MethodSegment[] => {
|
||||
for (let i = 0; i < periodIdxs.length - 1; ++i) {
|
||||
const start = periodIdxs[i] + 1;
|
||||
const end = periodIdxs[i + 1];
|
||||
const range = { line: item.row, charRange: { start, end } };
|
||||
const range = { startLine: item.row, startCol: start, endCol: end };
|
||||
segments.push({
|
||||
str: item.line.slice(start, end).trim(),
|
||||
location: range,
|
||||
@ -248,7 +251,8 @@ const serializeMethodOps = (stack: CodeStack): MethodSegment[] => {
|
||||
const parseServesLine = (stack: CodeStack): T.ChefRecipeServes => {
|
||||
const { line, row } = popCodeStack(stack, true);
|
||||
const match = line!.match(/^Serves (\d+).$/);
|
||||
if (!match) throw new ParseError("Malformed serves statement", { line: row });
|
||||
if (!match)
|
||||
throw new ParseError("Malformed serves statement", { startLine: row });
|
||||
return { line: row, num: parseInt(match[1], 10) };
|
||||
};
|
||||
|
||||
|
@ -96,7 +96,7 @@ export default class ChefLanguageEngine implements LanguageEngine<T.ChefRS> {
|
||||
const currFrame = this.getCurrentFrame();
|
||||
if (currFrame.pc === currFrame.recipe.method.length) {
|
||||
// Next step is "Serves" statement
|
||||
nextStepLocation = { line: currFrame.recipe.serves!.line + 1 };
|
||||
nextStepLocation = { startLine: currFrame.recipe.serves!.line };
|
||||
} else {
|
||||
// Next step is a regular method instruction
|
||||
const nextOp = currFrame.recipe.method[currFrame.pc];
|
||||
|
@ -30,11 +30,11 @@ describe("Parsing entire programs", () => {
|
||||
expect(method.length).toBe(14);
|
||||
expect(method.slice(0, 12).every((m) => m.op.code === "PUSH")).toBe(true);
|
||||
expect(method[12].op.code).toBe("LIQ-BOWL");
|
||||
expect(method[12].location.line).toBe(17);
|
||||
expect([403, 404]).toContain(method[12].location.charRange?.start);
|
||||
expect([439, 440]).toContain(method[12].location.charRange?.end);
|
||||
expect(method[12].location.startLine).toBe(17);
|
||||
expect([403, 404]).toContain(method[12].location.startCol);
|
||||
expect([439, 440]).toContain(method[12].location.endCol);
|
||||
expect(method[13].op.code).toBe("COPY");
|
||||
expect(method[13].location.line).toBe(17);
|
||||
expect(method[13].location.startLine).toBe(17);
|
||||
});
|
||||
|
||||
test("Fibonacci Du Fromage", () => {
|
||||
@ -54,13 +54,13 @@ describe("Parsing entire programs", () => {
|
||||
const mainMethod = program.main.method;
|
||||
expect(mainMethod.length).toBe(19);
|
||||
expect(mainMethod[0].op.code).toBe("STDIN");
|
||||
expect(mainMethod[0].location.line).toBe(10);
|
||||
expect(mainMethod[0].location.charRange?.start).toBe(0);
|
||||
expect(mainMethod[0].location.charRange?.end).toBe(30);
|
||||
expect(mainMethod[0].location.startLine).toBe(10);
|
||||
expect(mainMethod[0].location.startCol).toBe(0);
|
||||
expect(mainMethod[0].location.endCol).toBe(30);
|
||||
expect(mainMethod[18].op.code).toBe("COPY");
|
||||
expect(mainMethod[18].location.line).toBe(28);
|
||||
expect(mainMethod[18].location.charRange?.start).toBe(0);
|
||||
expect(mainMethod[18].location.charRange?.end).toBe(57);
|
||||
expect(mainMethod[18].location.startLine).toBe(28);
|
||||
expect(mainMethod[18].location.startCol).toBe(0);
|
||||
expect(mainMethod[18].location.endCol).toBe(57);
|
||||
|
||||
// Check loop jump addresses in method
|
||||
const mainOpener1 = mainMethod[8].op as LoopOpenOp;
|
||||
@ -85,13 +85,13 @@ describe("Parsing entire programs", () => {
|
||||
const auxMethod = program.auxes["salt and pepper"].method;
|
||||
expect(auxMethod.length).toBe(5);
|
||||
expect(auxMethod[0].op.code).toBe("POP");
|
||||
expect(auxMethod[0].location.line).toBe(39);
|
||||
expect(auxMethod[0].location.charRange?.start).toBe(0);
|
||||
expect(auxMethod[0].location.charRange?.end).toBe(26);
|
||||
expect(auxMethod[0].location.startLine).toBe(39);
|
||||
expect(auxMethod[0].location.startCol).toBe(0);
|
||||
expect(auxMethod[0].location.endCol).toBe(26);
|
||||
expect(auxMethod[4].op.code).toBe("ADD");
|
||||
expect(auxMethod[4].location.line).toBe(43);
|
||||
expect(auxMethod[4].location.charRange?.start).toBe(0);
|
||||
expect(auxMethod[4].location.charRange?.end).toBe(10);
|
||||
expect(auxMethod[4].location.startLine).toBe(43);
|
||||
expect(auxMethod[4].location.startCol).toBe(0);
|
||||
expect(auxMethod[4].location.endCol).toBe(10);
|
||||
});
|
||||
|
||||
test("Hello World Cake with Chocolate Sauce", () => {
|
||||
@ -112,13 +112,13 @@ describe("Parsing entire programs", () => {
|
||||
const mainMethod = program.main.method;
|
||||
expect(mainMethod.length).toBe(15);
|
||||
expect(mainMethod[0].op.code).toBe("PUSH");
|
||||
expect(mainMethod[0].location.line).toBe(27);
|
||||
expect(mainMethod[0].location.charRange?.start).toBe(0);
|
||||
expect(mainMethod[0].location.charRange?.end).toBe(40);
|
||||
expect(mainMethod[0].location.startLine).toBe(27);
|
||||
expect(mainMethod[0].location.startCol).toBe(0);
|
||||
expect(mainMethod[0].location.endCol).toBe(40);
|
||||
expect(mainMethod[14].op.code).toBe("FNCALL");
|
||||
expect(mainMethod[14].location.line).toBe(41);
|
||||
expect(mainMethod[14].location.charRange?.start).toBe(0);
|
||||
expect(mainMethod[14].location.charRange?.end).toBe(26);
|
||||
expect(mainMethod[14].location.startLine).toBe(41);
|
||||
expect(mainMethod[14].location.startCol).toBe(0);
|
||||
expect(mainMethod[14].location.endCol).toBe(26);
|
||||
|
||||
// Check loop jump addresses in method
|
||||
const mainOpener = mainMethod[12].op as LoopOpenOp;
|
||||
@ -142,13 +142,13 @@ describe("Parsing entire programs", () => {
|
||||
const auxMethod = program.auxes["chocolate sauce"].method;
|
||||
expect(auxMethod.length).toBe(13);
|
||||
expect(auxMethod[0].op.code).toBe("CLEAR");
|
||||
expect(auxMethod[0].location.line).toBe(53);
|
||||
expect(auxMethod[0].location.charRange?.start).toBe(0);
|
||||
expect(auxMethod[0].location.charRange?.end).toBe(21);
|
||||
expect(auxMethod[0].location.startLine).toBe(53);
|
||||
expect(auxMethod[0].location.startCol).toBe(0);
|
||||
expect(auxMethod[0].location.endCol).toBe(21);
|
||||
expect(auxMethod[12].op.code).toBe("END");
|
||||
expect(auxMethod[12].location.line).toBe(65);
|
||||
expect(auxMethod[12].location.charRange?.start).toBe(0);
|
||||
expect(auxMethod[12].location.charRange?.end).toBe(22);
|
||||
expect(auxMethod[12].location.startLine).toBe(65);
|
||||
expect(auxMethod[12].location.startCol).toBe(0);
|
||||
expect(auxMethod[12].location.endCol).toBe(22);
|
||||
|
||||
// Check loop jump addresses in method
|
||||
const auxOpener = auxMethod[4].op as LoopOpenOp;
|
||||
|
@ -42,8 +42,7 @@ export default class DeadfishLanguageEngine implements LanguageEngine<DFRS> {
|
||||
let nextStepLocation: DocumentRange | null = null;
|
||||
if (this._pc < this._ast.length) {
|
||||
const { line, char } = this._ast[this._pc].location;
|
||||
const charRange = { start: char, end: char + 1 };
|
||||
nextStepLocation = { line, charRange };
|
||||
nextStepLocation = { startLine: line, startCol: char, endCol: char + 1 };
|
||||
}
|
||||
|
||||
// Prepare and return execution result
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { LanguageEngine, StepExecutionResult } from "./types";
|
||||
import { DocumentRange, LanguageEngine, StepExecutionResult } from "./types";
|
||||
import {
|
||||
isParseError,
|
||||
isRuntimeError,
|
||||
@ -187,7 +187,7 @@ class ExecutionController<RS> {
|
||||
}
|
||||
|
||||
// Check if next line has breakpoint
|
||||
if (this._breakpoints.includes(this._result.nextStepLocation!.line)) {
|
||||
if (this.checkBreakpoint(this._result.nextStepLocation!)) {
|
||||
this._result.signal = "paused";
|
||||
return true;
|
||||
}
|
||||
@ -199,6 +199,19 @@ class ExecutionController<RS> {
|
||||
private sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/** Check if the given DocumentRange has a breakpoint */
|
||||
private checkBreakpoint(location: DocumentRange): boolean {
|
||||
if (location.endLine == null) {
|
||||
// Single line - just check if line is breakpoint
|
||||
return this._breakpoints.includes(location.startLine);
|
||||
} else {
|
||||
// Multiline - check if any line is breakpoint
|
||||
for (let line = location.startLine; line <= location.endLine; line++)
|
||||
if (this._breakpoints.includes(line)) return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ExecutionController;
|
||||
|
@ -2,20 +2,27 @@ import monaco from "monaco-editor";
|
||||
import React from "react";
|
||||
|
||||
/**
|
||||
* Type alias for defining range of characters in a single line.
|
||||
* - Missing `start` means range starting from start of the line.
|
||||
* - Missing `end` means range ending at the end of the line.
|
||||
*/
|
||||
export type CharRange = { start?: number; end?: number };
|
||||
|
||||
/**
|
||||
* Type denoting a range of text in document spanning within a line.
|
||||
* Type denoting a contiguous range of text in document.
|
||||
* All fields must be zero-indexed.
|
||||
*/
|
||||
export type DocumentRange = {
|
||||
/** Line number of the range */
|
||||
line: number;
|
||||
/** Section of line - omit to cover entire line */
|
||||
charRange?: CharRange;
|
||||
/** Line number on which the range starts */
|
||||
startLine: number;
|
||||
/**
|
||||
* Column number on which the range starts.
|
||||
* Omit to make the range start at the beginning of the line.
|
||||
*/
|
||||
startCol?: number;
|
||||
/**
|
||||
* Line number on which the range ends.
|
||||
* Omit to make the range end on the starting line.
|
||||
*/
|
||||
endLine?: number;
|
||||
/**
|
||||
* Column number on which the range ends.
|
||||
* Omit to make the range end at the end of the line.
|
||||
*/
|
||||
endCol?: number;
|
||||
};
|
||||
|
||||
/** Type denoting a document edit */
|
||||
|
@ -23,12 +23,15 @@ export const createHighlightRange = (
|
||||
highlights: DocumentRange
|
||||
): MonacoDecoration => {
|
||||
const location = get1IndexedLocation(highlights);
|
||||
const lineNum = location.line;
|
||||
const startChar = location.charRange?.start || 0;
|
||||
const endChar = location.charRange?.end || 1e5;
|
||||
const range = new monacoInstance.Range(lineNum, startChar, lineNum, endChar);
|
||||
const isWholeLine = !location.charRange;
|
||||
return { range, options: { isWholeLine, inlineClassName: "code-highlight" } };
|
||||
let { startLine, endLine, startCol, endCol } = location;
|
||||
const range = new monacoInstance.Range(
|
||||
startLine,
|
||||
startCol == null ? 1 : startCol,
|
||||
endLine == null ? startLine : endLine,
|
||||
endCol == null ? Infinity : endCol
|
||||
);
|
||||
// const isWholeLine = startCol == null && endCol == null;
|
||||
return { range, options: { inlineClassName: "code-highlight" } };
|
||||
};
|
||||
|
||||
/** Create Monaco decoration range object from highlights */
|
||||
@ -49,11 +52,12 @@ export const createValidationMarker = (
|
||||
range: DocumentRange
|
||||
): monaco.editor.IMarkerData => {
|
||||
const location = get1IndexedLocation(range);
|
||||
const { startLine, endLine, startCol, endCol } = location;
|
||||
return {
|
||||
startLineNumber: location.line,
|
||||
endLineNumber: location.line,
|
||||
startColumn: location.charRange?.start || 0,
|
||||
endColumn: location.charRange?.end || 1000,
|
||||
startLineNumber: startLine,
|
||||
endLineNumber: endLine == null ? startLine : endLine,
|
||||
startColumn: startCol == null ? 1 : startCol,
|
||||
endColumn: endCol == null ? Infinity : endCol,
|
||||
severity: monacoInstance.MarkerSeverity.Error,
|
||||
message: error.message,
|
||||
source: error.name,
|
||||
@ -69,13 +73,14 @@ export const createMonacoDocEdit = (
|
||||
edit: DocumentEdit
|
||||
): monaco.editor.IIdentifiedSingleEditOperation => {
|
||||
const location = get1IndexedLocation(edit.range);
|
||||
const { startLine, endLine, startCol, endCol } = location;
|
||||
return {
|
||||
text: edit.text,
|
||||
range: {
|
||||
startLineNumber: location.line,
|
||||
endLineNumber: location.line,
|
||||
startColumn: location.charRange?.start || 0,
|
||||
endColumn: location.charRange?.end || 1000,
|
||||
startLineNumber: startLine,
|
||||
endLineNumber: endLine == null ? startLine : endLine,
|
||||
startColumn: startCol == null ? 1 : startCol,
|
||||
endColumn: endCol == null ? Infinity : endCol,
|
||||
},
|
||||
};
|
||||
};
|
||||
@ -87,12 +92,10 @@ export const createMonacoDocEdit = (
|
||||
* @returns DocumentRange that uses 1-indexed values
|
||||
*/
|
||||
const get1IndexedLocation = (range: DocumentRange): DocumentRange => {
|
||||
const lineNum = range.line + 1;
|
||||
const charRange = range.charRange
|
||||
? {
|
||||
start: range.charRange.start ? range.charRange.start + 1 : undefined,
|
||||
end: range.charRange.end ? range.charRange.end + 1 : undefined,
|
||||
}
|
||||
: undefined;
|
||||
return { line: lineNum, charRange };
|
||||
return {
|
||||
startLine: range.startLine + 1,
|
||||
startCol: range.startCol == null ? 1 : range.startCol + 1,
|
||||
endLine: range.endLine == null ? range.startLine + 1 : range.endLine + 1,
|
||||
endCol: range.endCol == null ? Infinity : range.endCol + 1,
|
||||
};
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user