Skip to content

REPL History + Color formatting#19

Merged
frostney merged 1 commit into
mainfrom
fix-repl-ux
Feb 19, 2026
Merged

REPL History + Color formatting#19
frostney merged 1 commit into
mainfrom
fix-repl-ux

Conversation

@frostney
Copy link
Copy Markdown
Owner

@frostney frostney commented Feb 19, 2026

Summary by CodeRabbit

  • New Features
    • Interactive line editor with command history support and keyboard navigation
    • Colored output formatting for improved readability and type distinction
    • Enhanced terminal input with arrow keys for history navigation and cursor movement
    • Automatic history management with duplicate prevention and partial input restoration

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 19, 2026

📝 Walkthrough

Walkthrough

This PR adds interactive line editing and colored output formatting to the REPL. Two new units introduce a UNIX-terminal line editor with history support and a value formatter with ANSI color codes. The main REPL program is updated to integrate these components, replacing basic output with formatted results.

Changes

Cohort / File(s) Summary
REPL Line Editor & Formatter
units/Goccia.REPL.LineEditor.pas, units/Goccia.REPL.Formatter.pas
New line editor unit adds TLineEditor class with history management, UNIX terminal raw-mode handling, cursor control, and key-binding support (Enter, Ctrl-C, Ctrl-D, arrow keys, Home, End, etc.). New formatter unit adds FormatREPLValue function with ANSI color-coded output for various value types (undefined, null, string, number, boolean, symbol, function).
REPL Integration
REPL.dpr
Imports new formatter and line editor units; creates and manages TLineEditor instance; reads input via ReadLine with history tracking; replaces direct ToStringLiteral output calls with FormatREPLValue; adds version string to REPL header; preserves exception handling and timing display.

Sequence Diagram

sequenceDiagram
    participant User
    participant REPL as REPL.dpr
    participant Editor as TLineEditor
    participant Terminal
    participant Formatter as FormatREPLValue
    participant Engine as Goccia Engine

    loop REPL Session
        REPL->>Editor: ReadLine(prompt)
        Editor->>Terminal: Enter raw mode
        Editor->>Terminal: Display prompt
        User->>Terminal: Input characters
        Terminal->>Editor: Read character
        Editor->>Terminal: Redraw line (echo, cursor control)
        Editor->>Terminal: Handle history navigation (up/down arrows)
        Note over Editor: Store in history on Enter/Return
        Editor->>Terminal: Exit raw mode
        Editor-->>REPL: Return TLineReadResult (lrLine/lrExit)
        alt Input received (lrLine)
            REPL->>Engine: Execute source code
            Engine-->>REPL: Return TGocciaValue
            REPL->>Formatter: FormatREPLValue(value)
            Formatter-->>REPL: Colored string output
            REPL->>Terminal: Display formatted result
        else User exit (lrExit)
            REPL->>REPL: Break loop & cleanup
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

The review requires careful attention to the UNIX terminal handling logic in the line editor (raw mode, character reading, cursor positioning), history navigation implementation, and state management. The formatter logic is straightforward, but the line editor's interactive behavior and terminal-specific code paths (guarded by {$IFDEF UNIX}) demand thorough examination.

Poem

🐰 Hops with glee and terminal cheer,
History echoes, line-breaks appear!
Colors bloom where gray once lay,
The REPL dances in rainbow display!
Cursor-bounding, mode-raw and bright,
This fluffy editor's pure delight!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the two main additions: REPL history support (via TLineEditor) and color formatting (via FormatREPLValue).
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-repl-ux

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

Benchmark Results

134 benchmarks · 134 unchanged · avg -1.6%

arrays.js — 11 unchanged · avg -1.3%
Benchmark Base (ops/sec) PR (ops/sec) Change
Array.from length 100 146,059 143,228 -1.9%
Array.of 10 elements 159,652 158,382 -0.8%
spread into new array 171,170 165,686 -3.2%
map over 50 elements 91,270 90,980 -0.3%
filter over 50 elements 90,603 90,048 -0.6%
reduce sum 50 elements 95,472 95,706 +0.2%
forEach over 50 elements 95,688 95,704 +0.0%
find in 50 elements 100,117 100,220 +0.1%
sort 20 elements 4,090 3,991 -2.4%
flat nested array 81,643 79,523 -2.6%
flatMap 40,120 39,128 -2.5%
classes.js — 10 unchanged · avg -2.6%
Benchmark Base (ops/sec) PR (ops/sec) Change
simple class new 75,597 73,674 -2.5%
class with defaults 56,938 55,594 -2.4%
50 instances via Array.from 81,140 79,008 -2.6%
instance method call 34,754 33,754 -2.9%
static method call 60,697 59,974 -1.2%
single-level inheritance 29,532 28,641 -3.0%
two-level inheritance 27,716 27,035 -2.5%
private field access 36,626 35,718 -2.5%
private methods 43,265 41,678 -3.7%
getter/setter access 39,316 38,272 -2.7%
closures.js — 11 unchanged · avg -1.0%
Benchmark Base (ops/sec) PR (ops/sec) Change
closure over single variable 57,923 56,439 -2.6%
closure over multiple variables 56,876 55,872 -1.8%
nested closures 61,983 60,972 -1.6%
function as argument 42,799 42,191 -1.4%
function returning function 56,794 56,485 -0.5%
compose two functions 35,084 34,620 -1.3%
fn.call 83,420 82,056 -1.6%
fn.apply 61,906 61,190 -1.2%
fn.bind 71,214 72,530 +1.8%
recursive sum to 50 4,929 4,898 -0.6%
recursive tree traversal 8,662 8,615 -0.5%
collections.js — 12 unchanged · avg -0.6%
Benchmark Base (ops/sec) PR (ops/sec) Change
add 50 elements 84,319 83,690 -0.7%
has lookup (50 elements) 65,229 66,200 +1.5%
delete elements 65,712 64,865 -1.3%
forEach iteration 81,815 81,327 -0.6%
spread to array 88,092 86,652 -1.6%
deduplicate array 55,560 55,171 -0.7%
set 50 entries 83,739 82,790 -1.1%
get lookup (50 entries) 63,742 63,839 +0.2%
has check 69,691 70,196 +0.7%
delete entries 63,394 63,412 +0.0%
forEach iteration 77,436 76,368 -1.4%
keys/values/entries 51,326 50,548 -1.5%
destructuring.js — 14 unchanged · avg -1.8%
Benchmark Base (ops/sec) PR (ops/sec) Change
simple array destructuring 184,444 180,745 -2.0%
with rest element 137,841 135,591 -1.6%
with defaults 188,240 183,819 -2.3%
skip elements 202,890 198,145 -2.3%
nested array destructuring 111,124 108,994 -1.9%
swap variables 217,817 212,685 -2.4%
simple object destructuring 137,106 134,508 -1.9%
with defaults 165,209 162,827 -1.4%
with renaming 161,322 157,792 -2.2%
nested object destructuring 83,498 81,669 -2.2%
rest properties 85,289 83,549 -2.0%
object parameter 50,616 50,192 -0.8%
array parameter 66,751 66,185 -0.8%
mixed destructuring in map 88,515 87,529 -1.1%
fibonacci.js — 3 unchanged · avg -0.9%
Benchmark Base (ops/sec) PR (ops/sec) Change
recursive fib(15) 135 135 -0.3%
recursive fib(20) 12 12 -1.1%
iterative fib(20) via reduce 65,791 65,066 -1.1%
json.js — 11 unchanged · avg -2.0%
Benchmark Base (ops/sec) PR (ops/sec) Change
parse simple object 109,137 107,553 -1.5%
parse nested object 70,769 68,684 -2.9%
parse array of objects 40,164 38,786 -3.4%
parse large flat object 33,668 33,521 -0.4%
parse mixed types 51,825 50,103 -3.3%
stringify simple object 99,302 97,015 -2.3%
stringify nested object 56,719 55,088 -2.9%
stringify array of objects 108,230 107,683 -0.5%
stringify mixed types 48,200 47,223 -2.0%
parse then stringify 34,071 33,728 -1.0%
stringify then parse 45,649 44,804 -1.9%
jsx.jsx — 21 unchanged · avg -2.1%
Benchmark Base (ops/sec) PR (ops/sec) Change
simple element 114,809 110,811 -3.5%
self-closing element 118,212 114,845 -2.8%
element with string attribute 98,691 96,359 -2.4%
element with multiple attributes 85,214 83,639 -1.8%
element with expression attribute 88,982 87,113 -2.1%
text child 114,185 111,432 -2.4%
expression child 107,385 105,561 -1.7%
mixed text and expression 103,329 101,264 -2.0%
nested elements (3 levels) 44,010 42,955 -2.4%
sibling children 32,791 32,530 -0.8%
component element 83,067 80,611 -3.0%
component with children 51,814 50,342 -2.8%
dotted component 69,615 68,060 -2.2%
empty fragment 111,203 108,285 -2.6%
fragment with children 32,125 31,827 -0.9%
spread attributes 62,541 61,212 -2.1%
spread with overrides 54,319 53,388 -1.7%
shorthand props 84,435 82,538 -2.2%
nav bar structure 15,698 15,423 -1.8%
card component tree 18,391 18,096 -1.6%
10 list items via Array.from 140,203 139,499 -0.5%
numbers.js — 11 unchanged · avg -0.2%
Benchmark Base (ops/sec) PR (ops/sec) Change
integer arithmetic 178,136 175,312 -1.6%
floating point arithmetic 200,036 197,078 -1.5%
number coercion 103,158 105,932 +2.7%
toFixed 72,528 72,127 -0.6%
toString 99,280 99,234 -0.0%
valueOf 136,049 129,237 -5.0%
toPrecision 94,211 93,495 -0.8%
Number.isNaN 155,059 156,626 +1.0%
Number.isFinite 147,729 150,264 +1.7%
Number.isInteger 153,577 155,114 +1.0%
Number.parseInt and parseFloat 139,267 140,648 +1.0%
objects.js — 7 unchanged · avg -2.4%
Benchmark Base (ops/sec) PR (ops/sec) Change
create simple object 228,598 220,401 -3.6%
create nested object 121,636 116,714 -4.0%
create 50 objects via Array.from 141,761 140,291 -1.0%
property read 100,980 100,503 -0.5%
Object.keys 71,509 70,592 -1.3%
Object.entries 48,065 46,164 -4.0%
spread operator 91,968 89,876 -2.3%
promises.js — 12 unchanged · avg -3.1%
Benchmark Base (ops/sec) PR (ops/sec) Change
Promise.resolve(value) 303,625 302,555 -0.4%
new Promise(resolve => resolve(value)) 121,564 117,911 -3.0%
Promise.reject(reason) 318,031 315,613 -0.8%
resolve + then (1 handler) 105,007 103,354 -1.6%
resolve + then chain (3 deep) 43,837 42,169 -3.8%
resolve + then chain (10 deep) 14,612 13,883 -5.0%
reject + catch + then 65,092 62,968 -3.3%
resolve + finally + then 57,645 56,137 -2.6%
Promise.all (5 resolved) 23,342 22,323 -4.4%
Promise.race (5 resolved) 24,523 23,522 -4.1%
Promise.allSettled (5 mixed) 18,800 17,993 -4.3%
Promise.any (5 mixed) 23,246 22,198 -4.5%
strings.js — 11 unchanged · avg -0.7%
Benchmark Base (ops/sec) PR (ops/sec) Change
string concatenation 248,927 240,181 -3.5%
template literal 249,928 249,115 -0.3%
string repeat 235,848 233,896 -0.8%
split and join 102,507 100,268 -2.2%
indexOf and includes 106,897 106,713 -0.2%
toUpperCase and toLowerCase 156,073 154,202 -1.2%
slice and substring 95,933 96,082 +0.2%
trim operations 115,751 115,655 -0.1%
replace and replaceAll 136,224 133,799 -1.8%
startsWith and endsWith 84,914 85,809 +1.1%
padStart and padEnd 122,406 123,175 +0.6%

Measured on ubuntu-latest x64. Changes within ±7% are considered insignificant.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
units/Goccia.REPL.LineEditor.pas (1)

21-22: Mark ACursorPos as const.
The parameter isn’t modified. As per coding guidelines: Prefer const parameters for all parameters that are not modified in the function body.

♻️ Suggested update
-    procedure RedrawLine(const APrompt, ABuffer: string; ACursorPos: Integer);
+    procedure RedrawLine(const APrompt, ABuffer: string; const ACursorPos: Integer);
-procedure TLineEditor.RedrawLine(const APrompt, ABuffer: string; ACursorPos: Integer);
+procedure TLineEditor.RedrawLine(const APrompt, ABuffer: string; const ACursorPos: Integer);

Also applies to: 80-81

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@units/Goccia.REPL.LineEditor.pas` around lines 21 - 22, Change the ACursorPos
parameter to a const parameter wherever it’s declared and implemented (e.g.,
procedure RedrawLine(const APrompt, ABuffer: string; const ACursorPos: Integer)
and its implementation), and do the same for the other routine(s) that accept
ACursorPos (the occurrence mentioned around lines 80-81); make sure the
interface and implementation signatures match and update any forward
declarations so all ACursorPos parameters are declared as const.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@units/Goccia.REPL.LineEditor.pas`:
- Around line 10-29: Rename the class TLineEditor to TGocciaLineEditor and
update all corresponding declarations and references: change the class
declaration name to TGocciaLineEditor, update every
method/constructor/destructor definition from "procedure/function
TLineEditor.XXX" to "procedure/function TGocciaLineEditor.XXX" (including
Create, Destroy, ReadLine, AddToHistory, SetRawMode, RestoreTerminal, ReadChar,
RedrawLine, ReadLineRaw), and update the variable declaration and instantiation
in REPL.dpr to use TGocciaLineEditor instead of TLineEditor so all types and
calls match the new TGoccia<Name> prefix.

---

Nitpick comments:
In `@units/Goccia.REPL.LineEditor.pas`:
- Around line 21-22: Change the ACursorPos parameter to a const parameter
wherever it’s declared and implemented (e.g., procedure RedrawLine(const
APrompt, ABuffer: string; const ACursorPos: Integer) and its implementation),
and do the same for the other routine(s) that accept ACursorPos (the occurrence
mentioned around lines 80-81); make sure the interface and implementation
signatures match and update any forward declarations so all ACursorPos
parameters are declared as const.

Comment on lines +10 to +29
type
TLineReadResult = (lrLine, lrExit);

TLineEditor = class
private
FHistory: TStringList;
FHistoryIndex: Integer;
{$IFDEF UNIX}
procedure SetRawMode;
procedure RestoreTerminal;
function ReadChar: Char;
procedure RedrawLine(const APrompt, ABuffer: string; ACursorPos: Integer);
function ReadLineRaw(const APrompt: string; out ALine: string): TLineReadResult;
{$ENDIF}
public
constructor Create;
destructor Destroy; override;
function ReadLine(const APrompt: string; out ALine: string): TLineReadResult;
procedure AddToHistory(const ALine: string);
end;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n --type=pas --type=dpr "TLineEditor" -C 2

Repository: frostney/GocciaScript

Length of output: 92


🏁 Script executed:

rg -n "TLineEditor" --glob="*.pas" --glob="*.dpr" -C 2

Repository: frostney/GocciaScript

Length of output: 3203


Rename TLineEditor to TGocciaLineEditor.
Class names must use the TGoccia<Name> prefix. Update the class declaration in units/Goccia.REPL.LineEditor.pas (line 13), all method/constructor/destructor definitions within that file (lines 45, 62, 68, 80, 93, 274, 280, 286, 297), and the variable declaration and instantiation in REPL.dpr (lines 24, 39).

♻️ Suggested rename (apply consistently)
-  TLineEditor = class
+  TGocciaLineEditor = class

All method definitions must be updated from procedure TLineEditor.MethodName to procedure TGocciaLineEditor.MethodName, and so on.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
type
TLineReadResult = (lrLine, lrExit);
TLineEditor = class
private
FHistory: TStringList;
FHistoryIndex: Integer;
{$IFDEF UNIX}
procedure SetRawMode;
procedure RestoreTerminal;
function ReadChar: Char;
procedure RedrawLine(const APrompt, ABuffer: string; ACursorPos: Integer);
function ReadLineRaw(const APrompt: string; out ALine: string): TLineReadResult;
{$ENDIF}
public
constructor Create;
destructor Destroy; override;
function ReadLine(const APrompt: string; out ALine: string): TLineReadResult;
procedure AddToHistory(const ALine: string);
end;
type
TLineReadResult = (lrLine, lrExit);
TGocciaLineEditor = class
private
FHistory: TStringList;
FHistoryIndex: Integer;
{$IFDEF UNIX}
procedure SetRawMode;
procedure RestoreTerminal;
function ReadChar: Char;
procedure RedrawLine(const APrompt, ABuffer: string; ACursorPos: Integer);
function ReadLineRaw(const APrompt: string; out ALine: string): TLineReadResult;
{$ENDIF}
public
constructor Create;
destructor Destroy; override;
function ReadLine(const APrompt: string; out ALine: string): TLineReadResult;
procedure AddToHistory(const ALine: string);
end;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@units/Goccia.REPL.LineEditor.pas` around lines 10 - 29, Rename the class
TLineEditor to TGocciaLineEditor and update all corresponding declarations and
references: change the class declaration name to TGocciaLineEditor, update every
method/constructor/destructor definition from "procedure/function
TLineEditor.XXX" to "procedure/function TGocciaLineEditor.XXX" (including
Create, Destroy, ReadLine, AddToHistory, SetRawMode, RestoreTerminal, ReadChar,
RedrawLine, ReadLineRaw), and update the variable declaration and instantiation
in REPL.dpr to use TGocciaLineEditor instead of TLineEditor so all types and
calls match the new TGoccia<Name> prefix.

@frostney frostney merged commit 55beb14 into main Feb 19, 2026
4 checks passed
@frostney frostney deleted the fix-repl-ux branch February 19, 2026 21:14
@frostney frostney added the new feature New feature or request label Apr 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant