From 38b147c3e756e70323682d6e62ef8fb42871f4d8 Mon Sep 17 00:00:00 2001 From: Yura Dupyn <2153100+omedusyo@users.noreply.github.com> Date: Mon, 6 Apr 2026 16:31:32 +0200 Subject: [PATCH] Switch to `omedusyo/source-region` lib --- .gitmodules | 3 + libs/source-region | 1 + package-lock.json | 15 +- package.json | 3 +- src/lang/debug/repl.ts | 2 +- src/lang/expr.ts | 2 +- src/lang/parser/cursor.test.ts | 2 +- src/lang/parser/cursor.ts | 4 +- src/lang/parser/parser.ts | 2 +- src/lang/parser/scanner.ts | 4 +- src/lang/parser/source_text.ts | 239 ------------------ src/ui/Component/LineView.tsx | 2 +- src/ui/Component/ParseError.tsx | 2 +- src/ui/Digith/DigithError.tsx | 2 +- src/ui/Digith/Function/FunctionDigith.tsx | 2 +- .../Function/NewFunctionDraftDigith.tsx | 2 +- src/ui/Digith/REPL.tsx | 2 +- src/ui/Digith/Signal/NewSignalDraftDigith.tsx | 2 +- src/ui/Digith/Signal/SignalDigith.tsx | 2 +- src/ui/validation/helpers.ts | 2 +- 20 files changed, 37 insertions(+), 258 deletions(-) create mode 100644 .gitmodules create mode 160000 libs/source-region delete mode 100644 src/lang/parser/source_text.ts diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..ee62bf6 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "libs/source-region"] + path = libs/source-region + url = ssh://forgejo@git.meatbagoverclocked.com/omedusyo/source-region.git diff --git a/libs/source-region b/libs/source-region new file mode 160000 index 0000000..beab541 --- /dev/null +++ b/libs/source-region @@ -0,0 +1 @@ +Subproject commit beab541907abc8b204d63050e4d4dab82cc7db50 diff --git a/package-lock.json b/package-lock.json index 7b12155..702fd1a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,8 @@ "@replit/codemirror-vim": "^6.3.0", "codemirror": "^6.0.2", "electron-squirrel-startup": "^1.0.1", - "solid-js": "^1.9.11" + "solid-js": "^1.9.11", + "source-text": "file:libs/source-region" }, "devDependencies": { "@electron-forge/cli": "^7.11.1", @@ -42,6 +43,14 @@ "vite-plugin-solid": "^2.11.10" } }, + "libs/source-region": { + "name": "source-region", + "version": "0.1.0", + "license": "GPL-3.0-or-later", + "devDependencies": { + "typescript": "^5.4.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -11008,6 +11017,10 @@ "source-map": "^0.6.0" } }, + "node_modules/source-text": { + "resolved": "libs/source-region", + "link": true + }, "node_modules/spdx-correct": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", diff --git a/package.json b/package.json index c62da31..fe37a56 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@replit/codemirror-vim": "^6.3.0", "codemirror": "^6.0.2", "electron-squirrel-startup": "^1.0.1", - "solid-js": "^1.9.11" + "solid-js": "^1.9.11", + "source-text": "file:libs/source-region" } } diff --git a/src/lang/debug/repl.ts b/src/lang/debug/repl.ts index 54270cd..4ff8b85 100644 --- a/src/lang/debug/repl.ts +++ b/src/lang/debug/repl.ts @@ -2,7 +2,7 @@ import * as readline from 'readline'; import * as fs from 'fs'; import { parseExpr, ParseError } from '../parser/parser'; -import { SourceText, renderSpan, sourceText } from '../parser/source_text'; +import { SourceText, renderSpan, sourceText } from 'source-text'; import { exprToString } from '../debug/expr_show'; import { valueToString } from '../debug/value_show'; import { Program } from '../program'; diff --git a/src/lang/expr.ts b/src/lang/expr.ts index 21ebbcd..f7b0c7d 100644 --- a/src/lang/expr.ts +++ b/src/lang/expr.ts @@ -1,4 +1,4 @@ -import { Span } from "./parser/source_text" +import { Span } from "source-text" // === Identifiers === export type VariableName = string diff --git a/src/lang/parser/cursor.test.ts b/src/lang/parser/cursor.test.ts index ae32bf8..0ea509d 100644 --- a/src/lang/parser/cursor.test.ts +++ b/src/lang/parser/cursor.test.ts @@ -1,5 +1,5 @@ // AI GENERATED -import { SourceText } from "./source_text"; +import { SourceText } from "source-text"; import { Cursor, scanString, scanNumber } from "./cursor"; import { Result } from "../result"; diff --git a/src/lang/parser/cursor.ts b/src/lang/parser/cursor.ts index f89242c..02f96b7 100644 --- a/src/lang/parser/cursor.ts +++ b/src/lang/parser/cursor.ts @@ -1,5 +1,5 @@ -import { char, NEW_LINE, CARRIAGE_RETURN, DOT, DIGIT_0, DIGIT_9, LOWERCASE_a, LOWERCASE_f, UPPERCASE_A, UPPERCASE_F, SPACE, TAB } from './source_text'; -import type { SourceText, Span, SourceLocation, CodePoint, StringIndex, CodePointIndex } from './source_text'; +import { char, NEW_LINE, CARRIAGE_RETURN, DOT, DIGIT_0, DIGIT_9, LOWERCASE_a, LOWERCASE_f, UPPERCASE_A, UPPERCASE_F, SPACE, TAB } from 'source-text'; +import type { SourceText, Span, SourceLocation, CodePoint, StringIndex, CodePointIndex } from 'source-text'; import { Result } from '../result'; export type CursorState = { diff --git a/src/lang/parser/parser.ts b/src/lang/parser/parser.ts index f88003d..3b0313b 100644 --- a/src/lang/parser/parser.ts +++ b/src/lang/parser/parser.ts @@ -1,6 +1,6 @@ import { Cursor } from './cursor'; import { ExprScanError, exprStart, ExprStartToken, IdentifierKind, identifierScanner, isNextTokenExprStart, isNextTokenProductPatternStart, patternStart, PatternStartToken, signalExprStart, SignalExprStartToken, skipWhitespaceAndComments } from './scanner'; -import { char, CodePoint, SourceText, Span } from './source_text'; +import { char, CodePoint, SourceText, Span } from 'source-text'; import { Result } from '../result'; import { Expr, ExprBinding, FieldAssignment, FieldPattern, FunctionName, MatchBranch, Pattern, ProductPattern, SignalExpr, SignalExprBinding } from '../expr'; diff --git a/src/lang/parser/scanner.ts b/src/lang/parser/scanner.ts index 6ad1154..b9c6abe 100644 --- a/src/lang/parser/scanner.ts +++ b/src/lang/parser/scanner.ts @@ -1,5 +1,5 @@ -import { CARRIAGE_RETURN, char, NEW_LINE } from './source_text'; -import type { Span, CodePoint } from './source_text'; +import { CARRIAGE_RETURN, char, NEW_LINE } from 'source-text'; +import type { Span, CodePoint } from 'source-text'; import { isDigit, isWhitespace, scanNumber, scanString } from './cursor'; import type { Cursor, GenericScanError, NumberError, StringError } from './cursor'; diff --git a/src/lang/parser/source_text.ts b/src/lang/parser/source_text.ts deleted file mode 100644 index d99eb10..0000000 --- a/src/lang/parser/source_text.ts +++ /dev/null @@ -1,239 +0,0 @@ - -// === Char type === -export type StringIndex = number; // UTF-16 index into string -export type CodePointIndex = number; // index into array of code-points -export type CodePoint = number; // could also name it `UnicodeCodePoint`. Basically for `s: string` we have `s.codePointAt(i: index): char`. - -export function char(c: string): CodePoint { - return c.codePointAt(0) as CodePoint; -} - -export type CodePointRef = { - char: CodePoint, - offset: StringIndex, -}; - -// === Source Text === -export class SourceText { - readonly source: string; - // TODO: Later you can try to change this to two `Uint32Array`s - one for codepoints (each 20 bit but whatever), the other for pointers to original string. - readonly chars: CodePointRef[]; - - // Stores the CodePointIndex where each line begins - readonly lineStarts: CodePointIndex[]; - - constructor(rawSource: string) { - const source = rawSource.normalize('NFC'); - - this.source = source; - this.chars = []; - this.lineStarts = [0]; // Line 1 always starts at index 0 - - let i = 0; - while (i < source.length) { - const char = source.codePointAt(i) as CodePoint; - const cpIndex = this.chars.length; - this.chars.push({ char: char, offset: i }); - - const size =(char > 0xFFFF ? 2 : 1); - i += size; - - // === Newline Logic === - if (char === NEW_LINE) { - // Found a newline, the NEXT char starts a new line - this.lineStarts.push(cpIndex + 1); - } - // Handle CR (Classic Mac) or CRLF start - else if (char === CARRIAGE_RETURN) { - // Check if the next char is '\n' (CRLF) - // We peek ahead in the raw string to see if we need to skip the \n for line counting purposes - // or just treat this as a newline. - const nextIsNL = i < source.length && source.codePointAt(i) === NEW_LINE; - if (!nextIsNL) { - // Only push if it's NOT CRLF. If it is CRLF, the loop handles the \n next. - this.lineStarts.push(cpIndex + 1); - } - } - } - } - - get length(): number { - return this.chars.length; - } - - sliceByCp(start: number, end: number): string { - const startRef = this.chars[start]; - // Handle out of bounds gracefully - if (!startRef) return ""; - - const startOff = startRef.offset; - const endOff = end < this.chars.length - ? this.chars[end].offset - : this.source.length; - - return this.source.slice(startOff, endOff); - } - - // Converts a linear Code Point Index into SourceLocation - // getLocation(index: CodePointIndex): SourceLocation { - // // TODO: can be implemented either by a linear or binary search. - // return (0 as any); - // } - - // Returns the full text of a specific line (1-based index) - getLineText(line: number): string { - const lineIndex = line - 1; - if (lineIndex < 0 || lineIndex >= this.lineStarts.length) return ""; - - const startCp = this.lineStarts[lineIndex]; - const endCp = (lineIndex + 1 < this.lineStarts.length) - ? this.lineStarts[lineIndex + 1] - 1 // -1 to exclude the newline char itself - : this.chars.length; - - // TODO: Consider removing \r or \n from the end if they exist. - return this.sliceByCp(startCp, endCp); - } - - getLineRange(line: number): { start: CodePointIndex, end: CodePointIndex } { - const lineIndex = line - 1; - if (lineIndex < 0 || lineIndex >= this.lineStarts.length) { - // TODO: This is a bit suspicious. Maybe return undefined? - return { start: 0, end: 0 }; - } - - const start = this.lineStarts[lineIndex]; - const end = (lineIndex + 1 < this.lineStarts.length) - ? this.lineStarts[lineIndex + 1] - : this.chars.length; - - return { start, end }; - } -} - -export function sourceText(s: string): SourceText { - return new SourceText(s); -} - -export type Span = { - start: SourceLocation; - end: SourceLocation; -} - -export type SourceLocation = { - index: CodePointIndex; - line: number; // 1-based - column: number; // 1-based -} - -// Whitespace -export const NEW_LINE: CodePoint = char('\n'); -export const CARRIAGE_RETURN: CodePoint = char('\r'); -export const SPACE: CodePoint = char(' '); -export const TAB: CodePoint = char('\t'); - -// Digit Boundaries -export const DIGIT_0: CodePoint = char('0'); -export const DIGIT_9: CodePoint = char('9'); - -export const DOT: CodePoint = char('.'); - -// Hex Boundaries -export const LOWERCASE_a: CodePoint = char('a'); -export const UPPERCASE_A: CodePoint = char('A'); -export const LOWERCASE_f: CodePoint = char('f'); -export const UPPERCASE_F: CodePoint = char('F'); - -// === Rendering Utilities === - -export type LineView = { - lineNo: number; - sourceLine: string; // The full raw text of the line - - // These split the line into 3 parts for coloring: - // prefix | highlight | suffix - prefix: string; - highlight: string; - suffix: string; - - // Helpers for underlines (e.g., " ^^^^^") - gutterPad: string; // Padding to align line numbers - underline: string; // The literal "^^^" string for CLI usage -}; - -export function renderSpan(text: SourceText, span: Span, contextLines = 1): LineView[] { - const views: LineView[] = []; - - // Determine range of lines to show (including context) - const startLine = Math.max(1, span.start.line - contextLines); - const endLine = Math.min(text.lineStarts.length, span.end.line + contextLines); - - // Calculate the max width of line numbers for nice padding (e.g. " 9 |" vs " 10 |") - const maxLineNoWidth = endLine.toString().length; - - for (let lineNo = startLine; lineNo <= endLine; lineNo++) { - const lineRange = text.getLineRange(lineNo); - - // We strip the trailing newline for display purposes - let lineRaw = text.sliceByCp(lineRange.start, lineRange.end); - if (lineRaw.endsWith('\n') || lineRaw.endsWith('\r')) { - lineRaw = lineRaw.trimEnd(); - } - - // Determine the intersection of the Span with this specific Line - - // 1. Where does the highlight start on this line? - // If this is the start line, use span.column. Otherwise start at 0 (beginning of line) - // We subtract 1 because columns are 1-based, string indices are 0-based. - const highlightStartCol = (lineNo === span.start.line) - ? span.start.column - 1 - : 0; - - // 2. Where does the highlight end on this line? - // If this is the end line, use span.column. Otherwise end at the string length. - const highlightEndCol = (lineNo === span.end.line) - ? span.end.column - 1 - : lineRaw.length; - - // Logic to distinguish context lines from error lines - const isErrorLine = lineNo >= span.start.line && lineNo <= span.end.line; - - let prefix = "", highlight = "", suffix = ""; - - if (isErrorLine) { - // Clamp indices to bounds (safety) - const safeStart = Math.max(0, Math.min(highlightStartCol, lineRaw.length)); - const safeEnd = Math.max(0, Math.min(highlightEndCol, lineRaw.length)); - - prefix = lineRaw.substring(0, safeStart); - highlight = lineRaw.substring(safeStart, safeEnd); - suffix = lineRaw.substring(safeEnd); - } else { - // Pure context line - prefix = lineRaw; - } - - // Build the "underline" string (e.g., " ^^^^") - // Note: This naive approach assumes monospaced fonts and no fancy unicode widths, - // which usually holds for code. - let underline = ""; - if (isErrorLine) { - // Spaces for prefix - underline += " ".repeat(prefix.length); - // Carets for highlight (ensure at least 1 if it's a zero-width cursor position) - const hlLen = Math.max(1, highlight.length); - underline += "^".repeat(hlLen); - } - - views.push({ - lineNo, - sourceLine: lineRaw, - prefix, - highlight, - suffix, - gutterPad: " ".repeat(maxLineNoWidth - lineNo.toString().length), - underline - }); - } - - return views; -} diff --git a/src/ui/Component/LineView.tsx b/src/ui/Component/LineView.tsx index bc8f052..a41fe2c 100644 --- a/src/ui/Component/LineView.tsx +++ b/src/ui/Component/LineView.tsx @@ -1,4 +1,4 @@ -import { LineView } from "src/lang/parser/source_text"; +import { LineView } from "source-text"; import { For, Show } from "solid-js"; export function DisplayLineView(prop: { view: LineView }) { diff --git a/src/ui/Component/ParseError.tsx b/src/ui/Component/ParseError.tsx index f49cb7c..164d49a 100644 --- a/src/ui/Component/ParseError.tsx +++ b/src/ui/Component/ParseError.tsx @@ -1,5 +1,5 @@ import { ParseError } from "src/lang/parser/parser"; -import { renderSpan, SourceText } from "src/lang/parser/source_text"; +import { renderSpan, SourceText } from "source-text"; import { DisplayLineViews } from "./LineView"; export function formatErrorMesage(err: ParseError): string { diff --git a/src/ui/Digith/DigithError.tsx b/src/ui/Digith/DigithError.tsx index 231cd4a..e75d8ef 100644 --- a/src/ui/Digith/DigithError.tsx +++ b/src/ui/Digith/DigithError.tsx @@ -1,6 +1,6 @@ import { For, Match, Show, Switch } from "solid-js"; import { ParseError } from "src/lang/parser/parser"; -import { SourceText } from "src/lang/parser/source_text"; +import { SourceText } from "source-text"; import { ShowParseError } from 'src/ui/Component/ParseError'; import { Program } from "src/lang/program"; diff --git a/src/ui/Digith/Function/FunctionDigith.tsx b/src/ui/Digith/Function/FunctionDigith.tsx index 9c56e7a..a699ee8 100644 --- a/src/ui/Digith/Function/FunctionDigith.tsx +++ b/src/ui/Digith/Function/FunctionDigith.tsx @@ -2,7 +2,7 @@ import { createSignal, Show } from "solid-js"; import { Digith } from "src/ui/Digith"; import { useProgram } from "src/ui/ProgramProvider"; import { CodeEditor } from "src/ui/Component/CodeEditor"; -import { sourceText } from "src/lang/parser/source_text"; +import { sourceText } from "source-text"; import { Program } from "src/lang/program"; import { V, Validation, letValidate } from "src/ui/validation"; import { validateExprRaw, validateParamsRaw } from "src/ui/validation/helpers"; diff --git a/src/ui/Digith/Function/NewFunctionDraftDigith.tsx b/src/ui/Digith/Function/NewFunctionDraftDigith.tsx index 8dbe89d..c35a199 100644 --- a/src/ui/Digith/Function/NewFunctionDraftDigith.tsx +++ b/src/ui/Digith/Function/NewFunctionDraftDigith.tsx @@ -2,7 +2,7 @@ import { createSignal } from "solid-js"; import { Digith } from "src/ui/Digith"; import { useProgram } from "src/ui/ProgramProvider"; import { CodeEditor } from "src/ui/Component/CodeEditor"; -import { sourceText } from "src/lang/parser/source_text"; +import { sourceText } from "source-text"; import { Program } from "src/lang/program"; import { V, Validation, letValidate } from "src/ui/validation"; import { validateExprRaw, validateNameRaw, validateParamsRaw } from "src/ui/validation/helpers"; diff --git a/src/ui/Digith/REPL.tsx b/src/ui/Digith/REPL.tsx index 7dc9cd0..2adf2fb 100644 --- a/src/ui/Digith/REPL.tsx +++ b/src/ui/Digith/REPL.tsx @@ -3,7 +3,7 @@ import { useProgram } from 'src/ui/ProgramProvider'; import { eval_start } from 'src/lang/eval/evaluator'; import { Value } from 'src/lang/eval/value'; import { RuntimeError } from 'src/lang/eval/error'; -import { SourceText, sourceText } from 'src/lang/parser/source_text'; +import { SourceText, sourceText } from 'source-text'; import { ParseError, parseExpr } from 'src/lang/parser/parser'; import { ShowParseError } from 'src/ui/Component/ParseError'; import { Val } from 'src/ui/Component/Value'; diff --git a/src/ui/Digith/Signal/NewSignalDraftDigith.tsx b/src/ui/Digith/Signal/NewSignalDraftDigith.tsx index 9499379..4714111 100644 --- a/src/ui/Digith/Signal/NewSignalDraftDigith.tsx +++ b/src/ui/Digith/Signal/NewSignalDraftDigith.tsx @@ -2,7 +2,7 @@ import { createSignal } from "solid-js"; import { Digith } from "src/ui/Digith"; import { useProgram } from "src/ui/ProgramProvider"; import { CodeEditor } from "src/ui/Component/CodeEditor"; -import { sourceText } from "src/lang/parser/source_text"; +import { sourceText } from "source-text"; import { Program } from "src/lang/program"; import { V, Validation, letValidate } from "src/ui/validation"; import { validateNameRaw, validateSignalExprRaw } from "src/ui/validation/helpers"; diff --git a/src/ui/Digith/Signal/SignalDigith.tsx b/src/ui/Digith/Signal/SignalDigith.tsx index d323ebe..25d1972 100644 --- a/src/ui/Digith/Signal/SignalDigith.tsx +++ b/src/ui/Digith/Signal/SignalDigith.tsx @@ -2,7 +2,7 @@ import { createEffect, createSignal, onCleanup, Show } from "solid-js"; import { Digith } from "src/ui/Digith"; import { useProgram } from "src/ui/ProgramProvider"; import { CodeEditor } from "src/ui/Component/CodeEditor"; -import { sourceText } from "src/lang/parser/source_text"; +import { sourceText } from "source-text"; import { Program } from "src/lang/program"; import { V, Validation, letValidate } from "src/ui/validation"; import { validateSignalExprRaw } from "src/ui/validation/helpers"; diff --git a/src/ui/validation/helpers.ts b/src/ui/validation/helpers.ts index 60d2336..50b355a 100644 --- a/src/ui/validation/helpers.ts +++ b/src/ui/validation/helpers.ts @@ -1,5 +1,5 @@ import { ParseError, parseExpr, parseFunctionName, parseFunctionParameters, parseSignalExpr } from "src/lang/parser/parser"; -import { sourceText } from "src/lang/parser/source_text"; +import { sourceText } from "source-text"; import { Expr, FunctionName, ProductPattern, SignalExpr } from "src/lang/expr"; import { V } from "./";