feat: improve REPL with syntax highlighting and documentation

REPL improvements:
- Syntax highlighting for keywords (magenta), types (blue),
  strings (green), numbers (yellow), comments (gray)
- :doc command to show documentation for functions
- :browse command to list module exports
- Added docs for List, String, Option, Result, Console,
  Random, File, Http, Time, Sql, Postgres, Test modules

Example usage:
  lux> :doc List.map
  lux> :browse String
  lux> :doc fn

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-16 04:43:33 -05:00
parent b02807ebf4
commit 3ee3529ef6

View File

@@ -42,6 +42,8 @@ Commands:
:quit, :q Exit the REPL
:type <expr> Show the type of an expression
:info <name> Show info about a binding
:doc <name> Show documentation for a function/type
:browse <mod> List exports of a module (List, String, Option, etc.)
:env Show user-defined bindings
:clear Clear the environment
:load <file> Load and execute a file
@@ -1500,7 +1502,7 @@ impl LuxHelper {
let commands = vec![
":help", ":h", ":quit", ":q", ":type", ":t", ":clear", ":load", ":l",
":trace", ":traces", ":info", ":i", ":env",
":trace", ":traces", ":info", ":i", ":env", ":doc", ":d", ":browse", ":b",
]
.into_iter()
.map(String::from)
@@ -1588,6 +1590,100 @@ impl Highlighter for LuxHelper {
fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
Cow::Owned(format!("\x1b[90m{}\x1b[0m", hint))
}
fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> {
let mut result = String::with_capacity(line.len() * 2);
let mut chars = line.char_indices().peekable();
while let Some((i, c)) = chars.next() {
if c == '"' {
// String literal - highlight in green
result.push_str("\x1b[32m\"");
let mut escaped = false;
for (_, ch) in chars.by_ref() {
result.push(ch);
if escaped {
escaped = false;
} else if ch == '\\' {
escaped = true;
} else if ch == '"' {
break;
}
}
result.push_str("\x1b[0m");
} else if c == ':' && i == 0 {
// Command - highlight in cyan
result.push_str("\x1b[36m:");
for (_, ch) in chars.by_ref() {
if ch.is_whitespace() {
result.push_str("\x1b[0m");
result.push(ch);
break;
}
result.push(ch);
}
// Continue with the rest
for (_, ch) in chars.by_ref() {
result.push(ch);
}
} else if c.is_ascii_digit() || (c == '-' && chars.peek().map(|(_, ch)| ch.is_ascii_digit()).unwrap_or(false)) {
// Number - highlight in yellow
result.push_str("\x1b[33m");
result.push(c);
while let Some(&(_, ch)) = chars.peek() {
if ch.is_ascii_digit() || ch == '.' {
result.push(ch);
chars.next();
} else {
break;
}
}
result.push_str("\x1b[0m");
} else if c.is_alphabetic() || c == '_' {
// Identifier or keyword
let start = i;
let mut end = i + c.len_utf8();
while let Some(&(j, ch)) = chars.peek() {
if ch.is_alphanumeric() || ch == '_' {
end = j + ch.len_utf8();
chars.next();
} else {
break;
}
}
let word = &line[start..end];
// Check if it's a keyword
if self.keywords.contains(word) {
result.push_str("\x1b[35m"); // Magenta for keywords
result.push_str(word);
result.push_str("\x1b[0m");
} else if word.starts_with(char::is_uppercase) {
result.push_str("\x1b[34m"); // Blue for types/constructors
result.push_str(word);
result.push_str("\x1b[0m");
} else {
result.push_str(word);
}
} else if c == '/' && chars.peek().map(|(_, ch)| *ch == '/').unwrap_or(false) {
// Comment - highlight in gray
result.push_str("\x1b[90m");
result.push(c);
for (_, ch) in chars.by_ref() {
result.push(ch);
}
result.push_str("\x1b[0m");
} else {
result.push(c);
}
}
Cow::Owned(result)
}
fn highlight_char(&self, _line: &str, _pos: usize, _forced: bool) -> bool {
true
}
}
impl Validator for LuxHelper {}
@@ -1789,6 +1885,21 @@ fn handle_command(
interp.print_traces();
}
}
":doc" | ":d" => {
if let Some(name) = arg {
show_doc(name);
} else {
println!("Usage: :doc <name>");
}
}
":browse" | ":b" => {
if let Some(module) = arg {
browse_module(module);
} else {
println!("Usage: :browse <module>");
println!("Available modules: List, String, Option, Result, Console, Random, File, Http");
}
}
_ => {
println!("Unknown command: {}", cmd);
println!("Type :help for help");
@@ -1820,6 +1931,218 @@ fn show_environment(checker: &TypeChecker, helper: &LuxHelper) {
}
}
fn show_doc(name: &str) {
// Built-in documentation for common functions
let doc = match name {
// List functions
"List.length" => Some(("List.length : List<T> -> Int", "Returns the number of elements in a list.")),
"List.head" => Some(("List.head : List<T> -> Option<T>", "Returns the first element of a list, or None if empty.")),
"List.tail" => Some(("List.tail : List<T> -> Option<List<T>>", "Returns all elements except the first, or None if empty.")),
"List.map" => Some(("List.map : (List<A>, fn(A) -> B) -> List<B>", "Applies a function to each element, returning a new list.")),
"List.filter" => Some(("List.filter : (List<T>, fn(T) -> Bool) -> List<T>", "Returns elements for which the predicate returns true.")),
"List.fold" => Some(("List.fold : (List<T>, U, fn(U, T) -> U) -> U", "Reduces a list to a single value using an accumulator function.")),
"List.reverse" => Some(("List.reverse : List<T> -> List<T>", "Returns a new list with elements in reverse order.")),
"List.concat" => Some(("List.concat : (List<T>, List<T>) -> List<T>", "Concatenates two lists.")),
"List.take" => Some(("List.take : (List<T>, Int) -> List<T>", "Returns the first n elements.")),
"List.drop" => Some(("List.drop : (List<T>, Int) -> List<T>", "Returns all elements after the first n.")),
"List.get" => Some(("List.get : (List<T>, Int) -> Option<T>", "Returns the element at index n, or None if out of bounds.")),
"List.contains" => Some(("List.contains : (List<T>, T) -> Bool", "Returns true if the list contains the element.")),
"List.all" => Some(("List.all : (List<T>, fn(T) -> Bool) -> Bool", "Returns true if all elements satisfy the predicate.")),
"List.any" => Some(("List.any : (List<T>, fn(T) -> Bool) -> Bool", "Returns true if any element satisfies the predicate.")),
"List.find" => Some(("List.find : (List<T>, fn(T) -> Bool) -> Option<T>", "Returns the first element satisfying the predicate.")),
"List.sort" => Some(("List.sort : List<Int> -> List<Int>", "Sorts a list of integers in ascending order.")),
"List.range" => Some(("List.range : (Int, Int) -> List<Int>", "Creates a list of integers from start to end (exclusive).")),
// String functions
"String.length" => Some(("String.length : String -> Int", "Returns the number of characters in a string.")),
"String.concat" => Some(("String.concat : (String, String) -> String", "Concatenates two strings.")),
"String.substring" => Some(("String.substring : (String, Int, Int) -> String", "Returns a substring from start to end index.")),
"String.split" => Some(("String.split : (String, String) -> List<String>", "Splits a string by a delimiter.")),
"String.join" => Some(("String.join : (List<String>, String) -> String", "Joins a list of strings with a delimiter.")),
"String.trim" => Some(("String.trim : String -> String", "Removes leading and trailing whitespace.")),
"String.contains" => Some(("String.contains : (String, String) -> Bool", "Returns true if the string contains the substring.")),
"String.replace" => Some(("String.replace : (String, String, String) -> String", "Replaces all occurrences of a pattern.")),
"String.startsWith" => Some(("String.startsWith : (String, String) -> Bool", "Returns true if the string starts with the prefix.")),
"String.endsWith" => Some(("String.endsWith : (String, String) -> Bool", "Returns true if the string ends with the suffix.")),
"String.toUpper" => Some(("String.toUpper : String -> String", "Converts to uppercase.")),
"String.toLower" => Some(("String.toLower : String -> String", "Converts to lowercase.")),
// Option functions
"Option.map" => Some(("Option.map : (Option<A>, fn(A) -> B) -> Option<B>", "Applies a function to the value inside Some, or returns None.")),
"Option.flatMap" => Some(("Option.flatMap : (Option<A>, fn(A) -> Option<B>) -> Option<B>", "Like map, but the function returns an Option.")),
"Option.getOrElse" => Some(("Option.getOrElse : (Option<T>, T) -> T", "Returns the value inside Some, or the default if None.")),
"Option.isSome" => Some(("Option.isSome : Option<T> -> Bool", "Returns true if the value is Some.")),
"Option.isNone" => Some(("Option.isNone : Option<T> -> Bool", "Returns true if the value is None.")),
// Result functions
"Result.map" => Some(("Result.map : (Result<A, E>, fn(A) -> B) -> Result<B, E>", "Applies a function to the Ok value.")),
"Result.flatMap" => Some(("Result.flatMap : (Result<A, E>, fn(A) -> Result<B, E>) -> Result<B, E>", "Like map, but the function returns a Result.")),
"Result.getOrElse" => Some(("Result.getOrElse : (Result<T, E>, T) -> T", "Returns the Ok value, or the default if Err.")),
"Result.isOk" => Some(("Result.isOk : Result<T, E> -> Bool", "Returns true if the value is Ok.")),
"Result.isErr" => Some(("Result.isErr : Result<T, E> -> Bool", "Returns true if the value is Err.")),
// Effects
"Console.print" => Some(("Console.print : String -> Unit", "Prints a string to the console.")),
"Console.readLine" => Some(("Console.readLine : () -> String", "Reads a line from standard input.")),
"Random.int" => Some(("Random.int : (Int, Int) -> Int", "Generates a random integer in the range [min, max].")),
"Random.float" => Some(("Random.float : () -> Float", "Generates a random float in [0, 1).")),
"Random.bool" => Some(("Random.bool : () -> Bool", "Generates a random boolean.")),
"File.read" => Some(("File.read : String -> String", "Reads the contents of a file.")),
"File.write" => Some(("File.write : (String, String) -> Unit", "Writes content to a file.")),
"File.exists" => Some(("File.exists : String -> Bool", "Returns true if the file exists.")),
"Http.get" => Some(("Http.get : String -> String", "Performs an HTTP GET request.")),
"Http.post" => Some(("Http.post : (String, String) -> String", "Performs an HTTP POST request with a body.")),
"Time.now" => Some(("Time.now : () -> Int", "Returns the current Unix timestamp in milliseconds.")),
"Sql.open" => Some(("Sql.open : String -> SqlConn", "Opens a SQLite database file.")),
"Sql.openMemory" => Some(("Sql.openMemory : () -> SqlConn", "Opens an in-memory SQLite database.")),
"Sql.query" => Some(("Sql.query : (SqlConn, String) -> List<Row>", "Executes a SQL query and returns all rows.")),
"Sql.execute" => Some(("Sql.execute : (SqlConn, String) -> Int", "Executes a SQL statement and returns affected rows.")),
"Postgres.connect" => Some(("Postgres.connect : String -> Int", "Connects to a PostgreSQL database.")),
"Postgres.query" => Some(("Postgres.query : (Int, String) -> List<Row>", "Executes a SQL query on PostgreSQL.")),
"Postgres.execute" => Some(("Postgres.execute : (Int, String) -> Int", "Executes a SQL statement on PostgreSQL.")),
// Language constructs
"fn" => Some(("fn name(args): Type = body", "Defines a function.")),
"let" => Some(("let name = value", "Binds a value to a name.")),
"if" => Some(("if condition then expr1 else expr2", "Conditional expression.")),
"match" => Some(("match value { pattern => expr, ... }", "Pattern matching expression.")),
"effect" => Some(("effect Name { fn op(...): Type }", "Defines an effect with operations.")),
"handler" => Some(("handler { op => body }", "Defines an effect handler.")),
"with" => Some(("fn f(): T with {Effect1, Effect2}", "Declares effects a function may perform.")),
"type" => Some(("type Name<T> = ...", "Defines a type alias.")),
"run" => Some(("run expr with { handler }", "Executes an expression with effect handlers.")),
_ => None,
};
match doc {
Some((sig, desc)) => {
println!("\x1b[1m{}\x1b[0m", sig);
println!();
println!(" {}", desc);
}
None => {
println!("No documentation for '{}'", name);
println!("Try :doc List.map or :browse List");
}
}
}
fn browse_module(module: &str) {
let exports: Vec<(&str, &str)> = match module {
"List" => vec![
("length", "List<T> -> Int"),
("head", "List<T> -> Option<T>"),
("tail", "List<T> -> Option<List<T>>"),
("get", "(List<T>, Int) -> Option<T>"),
("map", "(List<A>, fn(A) -> B) -> List<B>"),
("filter", "(List<T>, fn(T) -> Bool) -> List<T>"),
("fold", "(List<T>, U, fn(U, T) -> U) -> U"),
("reverse", "List<T> -> List<T>"),
("concat", "(List<T>, List<T>) -> List<T>"),
("take", "(List<T>, Int) -> List<T>"),
("drop", "(List<T>, Int) -> List<T>"),
("contains", "(List<T>, T) -> Bool"),
("all", "(List<T>, fn(T) -> Bool) -> Bool"),
("any", "(List<T>, fn(T) -> Bool) -> Bool"),
("find", "(List<T>, fn(T) -> Bool) -> Option<T>"),
("sort", "List<Int> -> List<Int>"),
("range", "(Int, Int) -> List<Int>"),
("forEach", "(List<T>, fn(T) -> Unit) -> Unit"),
],
"String" => vec![
("length", "String -> Int"),
("concat", "(String, String) -> String"),
("substring", "(String, Int, Int) -> String"),
("split", "(String, String) -> List<String>"),
("join", "(List<String>, String) -> String"),
("trim", "String -> String"),
("contains", "(String, String) -> Bool"),
("replace", "(String, String, String) -> String"),
("startsWith", "(String, String) -> Bool"),
("endsWith", "(String, String) -> Bool"),
("toUpper", "String -> String"),
("toLower", "String -> String"),
("lines", "String -> List<String>"),
],
"Option" => vec![
("map", "(Option<A>, fn(A) -> B) -> Option<B>"),
("flatMap", "(Option<A>, fn(A) -> Option<B>) -> Option<B>"),
("getOrElse", "(Option<T>, T) -> T"),
("isSome", "Option<T> -> Bool"),
("isNone", "Option<T> -> Bool"),
],
"Result" => vec![
("map", "(Result<A, E>, fn(A) -> B) -> Result<B, E>"),
("flatMap", "(Result<A, E>, fn(A) -> Result<B, E>) -> Result<B, E>"),
("getOrElse", "(Result<T, E>, T) -> T"),
("isOk", "Result<T, E> -> Bool"),
("isErr", "Result<T, E> -> Bool"),
],
"Console" => vec![
("print", "String -> Unit"),
("readLine", "() -> String"),
],
"Random" => vec![
("int", "(Int, Int) -> Int"),
("float", "() -> Float"),
("bool", "() -> Bool"),
],
"File" => vec![
("read", "String -> String"),
("write", "(String, String) -> Unit"),
("exists", "String -> Bool"),
("delete", "String -> Unit"),
],
"Http" => vec![
("get", "String -> String"),
("post", "(String, String) -> String"),
],
"Time" => vec![
("now", "() -> Int"),
],
"Sql" => vec![
("open", "String -> SqlConn"),
("openMemory", "() -> SqlConn"),
("close", "SqlConn -> Unit"),
("query", "(SqlConn, String) -> List<Row>"),
("queryOne", "(SqlConn, String) -> Option<Row>"),
("execute", "(SqlConn, String) -> Int"),
("beginTx", "SqlConn -> Unit"),
("commit", "SqlConn -> Unit"),
("rollback", "SqlConn -> Unit"),
],
"Postgres" => vec![
("connect", "String -> Int"),
("close", "Int -> Unit"),
("query", "(Int, String) -> List<Row>"),
("queryOne", "(Int, String) -> Option<Row>"),
("execute", "(Int, String) -> Int"),
("beginTx", "Int -> Unit"),
("commit", "Int -> Unit"),
("rollback", "Int -> Unit"),
],
"Test" => vec![
("assert", "(Bool, String) -> Unit"),
("assertEqual", "(T, T) -> Unit"),
("assertTrue", "Bool -> Unit"),
("assertFalse", "Bool -> Unit"),
("fail", "String -> Unit"),
],
_ => {
println!("Unknown module: {}", module);
println!("Available modules: List, String, Option, Result, Console, Random, File, Http, Time, Sql, Postgres, Test");
return;
}
};
println!("\x1b[1mmodule {}\x1b[0m", module);
println!();
for (name, sig) in exports {
println!(" \x1b[34m{}.{}\x1b[0m : {}", module, name, sig);
}
}
fn show_type(expr_str: &str, checker: &mut TypeChecker) {
// Wrap expression in a let to parse it
let wrapped = format!("let _expr_ = {}", expr_str);