diff --git a/languages/befunge93/runtime.ts b/languages/befunge93/runtime.ts index 896f746..18f9125 100644 --- a/languages/befunge93/runtime.ts +++ b/languages/befunge93/runtime.ts @@ -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 */ diff --git a/languages/brainfuck/runtime.ts b/languages/brainfuck/runtime.ts index c322925..9415113 100644 --- a/languages/brainfuck/runtime.ts +++ b/languages/brainfuck/runtime.ts @@ -51,8 +51,7 @@ export default class BrainfuckLanguageEngine implements LanguageEngine { 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 { 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 { 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, }); } diff --git a/languages/chef/parser/index.ts b/languages/chef/parser/index.ts index 3be597d..db68daf 100644 --- a/languages/chef/parser/index.ts +++ b/languages/chef/parser/index.ts @@ -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) }; }; diff --git a/languages/chef/runtime/index.ts b/languages/chef/runtime/index.ts index 7b09db0..4cb8a7b 100644 --- a/languages/chef/runtime/index.ts +++ b/languages/chef/runtime/index.ts @@ -96,7 +96,7 @@ export default class ChefLanguageEngine implements LanguageEngine { 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]; diff --git a/languages/chef/tests/parser-index.test.ts b/languages/chef/tests/parser-index.test.ts index 5ea552e..ce14d89 100644 --- a/languages/chef/tests/parser-index.test.ts +++ b/languages/chef/tests/parser-index.test.ts @@ -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; diff --git a/languages/deadfish/runtime.ts b/languages/deadfish/runtime.ts index eadc44f..922325f 100644 --- a/languages/deadfish/runtime.ts +++ b/languages/deadfish/runtime.ts @@ -42,8 +42,7 @@ export default class DeadfishLanguageEngine implements LanguageEngine { 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 diff --git a/languages/execution-controller.ts b/languages/execution-controller.ts index 6ad5ed1..ce17d16 100644 --- a/languages/execution-controller.ts +++ b/languages/execution-controller.ts @@ -1,4 +1,4 @@ -import { LanguageEngine, StepExecutionResult } from "./types"; +import { DocumentRange, LanguageEngine, StepExecutionResult } from "./types"; import { isParseError, isRuntimeError, @@ -187,7 +187,7 @@ class ExecutionController { } // 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 { private sleep(ms: number): Promise { 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; diff --git a/languages/types.ts b/languages/types.ts index 24942f6..c9eb941 100644 --- a/languages/types.ts +++ b/languages/types.ts @@ -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 */ diff --git a/ui/code-editor/monaco-utils.ts b/ui/code-editor/monaco-utils.ts index e5c598f..6253147 100644 --- a/ui/code-editor/monaco-utils.ts +++ b/ui/code-editor/monaco-utils.ts @@ -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, + }; };