add /=| table and /=\ tree eval display formats with CORDIAL_GUIDE

This commit is contained in:
jess 2026-04-06 14:00:22 -07:00
parent a1ca3817ba
commit 81dbb41a65
6 changed files with 1094 additions and 28 deletions

781
CORDIAL_GUIDE.md Normal file
View File

@ -0,0 +1,781 @@
# Cordial Language Reference Guide
Cordial is a simple, expression-based programming language embedded in Swiftly. This guide documents every working feature.
## Evaluation Prefix
All evaluation in the editor uses the `/=` prefix:
```
/= 2 + 3
```
Output:
```
5
```
## Types
Cordial has five value types:
- **number**: floating-point numbers (internally f64). Displayed as integers when they have no fractional part.
- **bool**: `true` or `false`
- **str**: strings in double quotes, with escape sequences (`\n`, `\t`, `\\`, `\"`)
- **array**: ordered collections of values
- **void**: the value returned by statements with no explicit value
## Variables
### Declaration
Use `let` to declare and initialize a variable:
```
let x = 5
/= x
```
Output:
```
5
```
### Type Annotations
Optionally annotate the type after the variable name:
```
let x: int = 3.7
/= x
```
Output:
```
3
```
Supported type annotations:
- `int`: truncates numbers to integers
- `float`: accepts any number (no coercion)
- `bool`: converts 0 to `false`, 1 to `true`; rejects other numbers
- `str`: only accepts strings
```
let b: bool = 1
/= b
```
Output:
```
true
```
### Assignment
Reassign an existing variable without `let`:
```
let x = 5
x = 10
/= x
```
Output:
```
10
```
## Operators
### Arithmetic
- `+`: addition or string concatenation
- `-`: subtraction
- `*`: multiplication
- `/`: division (errors on division by zero)
- `%`: modulo
- `^`: exponentiation (right-associative)
```
/= 2 ^ 3 ^ 2
```
Output:
```
512
```
(Evaluated as 2 ^ (3 ^ 2) = 2 ^ 9 = 512)
### Comparison
All comparison operators return booleans:
- `<`: less than
- `>`: greater than
- `<=`: less than or equal
- `>=`: greater than or equal
- `==`: equality
- `!=`: inequality
```
/= 5 > 3
```
Output:
```
true
```
### Logical
- `&&`: logical AND (short-circuits)
- `||`: logical OR (short-circuits)
- `!`: logical NOT
```
/= true && false
```
Output:
```
false
```
```
/= !true
```
Output:
```
false
```
### Operator Precedence
From lowest to highest:
1. `||`
2. `&&`
3. `==`, `!=`, `<`, `>`, `<=`, `>=`
4. `+`, `-`
5. `*`, `/`, `%`
6. `^` (right-associative)
7. unary `-`, `!`
8. function calls, array literals
## Strings
String literals use double quotes:
```
/= "hello"
```
Output:
```
hello
```
Escape sequences:
- `\n`: newline
- `\t`: tab
- `\\`: backslash
- `\"`: double quote
### String Concatenation
Strings concatenate with the `+` operator. Numbers and booleans are automatically converted:
```
/= "value: " + 42
```
Output:
```
value: 42
```
```
/= 100 + " items"
```
Output:
```
100 items
```
## Arrays
### Literals
Create arrays with square brackets:
```
/= [1, 2, 3]
```
Output:
```
[1, 2, 3]
```
Arrays can contain mixed types:
```
/= [1, "two", true]
```
Output:
```
[1, "two", true]
```
Empty arrays:
```
/= []
```
Output:
```
[]
```
### Indexing
Access elements by index (zero-based):
```
let arr = [10, 20, 30]
/= arr[1]
```
Output:
```
20
```
Negative indices count from the end:
```
/= [10, 20, 30][-1]
```
Output:
```
30
```
Out-of-bounds access returns an error.
### String Indexing
Strings support the same index syntax, returning individual characters:
```
/= "hello"[0]
```
Output:
```
h
```
## Functions
### Definition
Define functions with the `fn` keyword:
```
fn add(a, b) {
a + b
}
```
The last expression in the function body is returned implicitly. An explicit `return` statement is also available for early exit.
```
fn square(x) {
x * x
}
/= square(5)
```
Output:
```
25
```
### Scope
Function parameters are local to the function. The function saves and restores the variable scope:
```
let x = 10
fn modify() {
x = 5
}
modify()
/= x
```
Output:
```
10
```
(x inside the function is a separate variable; the outer x is unchanged)
### Recursion
Functions can call themselves recursively, up to a maximum call depth of 256:
```
fn fib(n) {
let a = 0
let b = 1
let i = 0
while (i < n) {
let tmp = b
b = a + b
a = tmp
i = i + 1
}
a
}
/= fib(10)
```
Output:
```
55
```
## Control Flow
### While Loops
Loop while a condition is truthy:
```
let i = 0
let sum = 0
while (i < 10) {
sum = sum + i
i = i + 1
}
/= sum
```
Output:
```
45
```
Parentheses around the condition are optional (but recommended for clarity).
Loop safety: while loops are limited to 10,000 iterations to prevent infinite loops.
### If/Else
Conditional branching with optional else clause:
```
let x = 10
if x > 5 {
x = 1
} else {
x = 0
}
/= x
```
Output:
```
1
```
Parentheses around the condition are optional. Else-if chaining works:
```
let grade = 85
let letter = "F"
if grade >= 90 {
letter = "A"
} else if grade >= 80 {
letter = "B"
} else if grade >= 70 {
letter = "C"
}
/= letter
```
Output:
```
B
```
### For Loops
Iterate over arrays or ranges:
```
let sum = 0
for x in [1, 2, 3, 4, 5] {
sum = sum + x
}
/= sum
```
Output:
```
15
```
Using a range expression (`start..end`, exclusive of end):
```
let sum = 0
for i in 0..5 {
sum = sum + i
}
/= sum
```
Output:
```
10
```
The `range(start, end)` builtin function also works:
```
let sum = 0
for i in range(1, 6) {
sum = sum + i
}
/= sum
```
Output:
```
15
```
For loops are limited to 10,000 iterations.
### Return Statements
Use `return` for early exit from functions:
```
fn abs_val(x) {
if x < 0 {
return -x
}
x
}
/= abs_val(-7)
```
Output:
```
7
```
Bare `return` (with no value) returns `false`.
## Builtin Functions
### Math Functions
All math functions accept a single number and return a number:
- `sin(x)`: sine
- `cos(x)`: cosine
- `tan(x)`: tangent
- `asin(x)`: arc sine
- `acos(x)`: arc cosine
- `atan(x)`: arc tangent
- `sqrt(x)`: square root
- `abs(x)`: absolute value
- `floor(x)`: round down
- `ceil(x)`: round up
- `round(x)`: round to nearest integer
- `ln(x)`: natural logarithm
- `log(x)`: base-10 logarithm
```
/= sqrt(16)
```
Output:
```
4
```
```
/= abs(-5)
```
Output:
```
5
```
```
/= floor(3.7)
```
Output:
```
3
```
### Range Function
`range(start, end)` returns an array of numbers from start to end (exclusive):
```
/= range(0, 5)
```
Output:
```
[0, 1, 2, 3, 4]
```
### Length Function
`len()` returns the length of a string or array:
```
/= len("hello")
```
Output:
```
5
```
```
/= len([1, 2, 3])
```
Output:
```
3
```
## Truthiness
In boolean contexts (conditions, logical operators), values are truthy or falsy:
- **bool**: `true` is truthy, `false` is falsy
- **number**: 0 is falsy, all other numbers are truthy
- **string**: empty string is falsy, all other strings are truthy
- **array**: empty array is falsy, all other arrays are truthy
- **void**: falsy
- **error**: falsy
Truthiness matters for `if`, `while`, `for` conditions and `&&`/`||` operators.
## Ranges
The `..` operator creates an array from start to end (exclusive):
```
/= 0..5
```
Output:
```
[0, 1, 2, 3, 4]
```
Ranges are primarily useful with `for` loops and can be stored in variables.
## Negative Numbers
Negative number literals are recognized in appropriate contexts:
```
/= -5
```
Output:
```
-5
```
```
/= 10 + -3
```
Output:
```
7
```
The unary minus operator also works:
```
/= -(5 + 3)
```
Output:
```
-8
```
## Error Handling
Errors are propagated as error values and displayed with an `error:` prefix:
```
/= undefined_var
```
Output:
```
error: undefined variable 'undefined_var'
```
```
/= 1 / 0
```
Output:
```
error: division by zero
```
Multiple statements execute in sequence, and errors in one statement don't prevent subsequent statements from executing.
## Comments
Single-line comments use `//`:
```
// This is a comment
let x = 5
/= x
```
Comments can appear at the end of lines too.
## Examples
### Multi-step Calculation
```
let principal = 1000
let rate = 0.05
let years = 10
let amount = principal * (1 + rate) ^ years
/= amount
```
Output:
```
1628.89462382...
```
### Fibonacci
```
fn fib(n) {
let a = 0
let b = 1
let i = 0
while (i < n) {
let tmp = b
b = a + b
a = tmp
i = i + 1
}
a
}
/= fib(15)
```
Output:
```
610
```
### String Building
```
let greeting = "Hello"
let name = "World"
/= greeting + ", " + name + "!"
```
Output:
```
Hello, World!
```
## Display Formats
### Inline (default)
The standard `/=` prefix displays the result as a single value on the right edge of the editor:
```
let x = 42
/= x
```
Result appears as: `→ 42`
Arrays display inline:
```
/= [1, 2, 3]
```
Result: `→ [1, 2, 3]`
### Table (`/=|`)
The `/=|` prefix renders a 2D array (array of arrays) as a visual table overlay:
```
let data = [["Name", "Age"], ["Alice", 30], ["Bob", 25]]
/=| data
```
The first row is treated as the header (rendered bold). Each subsequent row becomes a table row. Non-array values fall back to inline display.
Works with any 2D array:
```
let matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
/=| matrix
```
### Tree (`/=\`)
The `/=\` prefix renders nested data as an indented tree overlay:
```
let tree = [1, [2, 3], [4, [5, 6]]]
/=\ tree
```
Nested arrays are expanded with tree-drawing characters showing depth. Leaf values display inline. The root shows the total element count.
## Limitations
- **No array mutation**: Arrays cannot be modified after creation (no `arr[i] = val`)
- **No mutable references**: Variable reassignment creates new bindings in the current scope
- **Maximum call depth**: 256 function calls deep
- **Maximum loop iterations**: 10,000 iterations per while/for loop
- **No user-defined types or structs**
- **No imports or modules**
- **No pattern matching or destructuring**
- **No closures or lambdas**

View File

@ -6,6 +6,7 @@ use crate::interp;
pub struct EvalResult {
pub line: usize,
pub result: String,
pub format: String,
}
#[derive(Debug, Clone, Serialize)]
@ -36,14 +37,23 @@ pub fn evaluate_document(text: &str) -> DocumentResult {
let interp_results = interp::interpret_document(&lines);
for ir in interp_results {
let fmt = match ir.format {
interp::EvalFormat::Inline => "inline",
interp::EvalFormat::Table => "table",
interp::EvalFormat::Tree => "tree",
};
match ir.value {
Some(interp::Value::Error(e)) => {
errors.push(EvalError { line: ir.line, error: e });
}
Some(v) => {
let s = v.display();
let s = match ir.format {
interp::EvalFormat::Table => value_to_table_json(&v),
interp::EvalFormat::Tree => value_to_tree_json(&v),
interp::EvalFormat::Inline => v.display(),
};
if !s.is_empty() {
results.push(EvalResult { line: ir.line, result: s });
results.push(EvalResult { line: ir.line, result: s, format: fmt.to_string() });
}
}
None => {}
@ -53,6 +63,41 @@ pub fn evaluate_document(text: &str) -> DocumentResult {
DocumentResult { results, errors }
}
fn value_to_table_json(val: &interp::Value) -> String {
match val {
interp::Value::Array(rows) => {
let table: Vec<Vec<String>> = rows.iter().map(|row| {
match row {
interp::Value::Array(cols) => cols.iter().map(|c| c.display()).collect(),
other => vec![other.display()],
}
}).collect();
serde_json::to_string(&table).unwrap_or_else(|_| val.display())
}
_ => val.display(),
}
}
fn value_to_tree_json(val: &interp::Value) -> String {
fn to_json(v: &interp::Value) -> serde_json::Value {
match v {
interp::Value::Array(items) => {
serde_json::Value::Array(items.iter().map(|i| to_json(i)).collect())
}
interp::Value::Number(n) => {
serde_json::Value::Number(
serde_json::Number::from_f64(*n)
.unwrap_or_else(|| serde_json::Number::from(0))
)
}
interp::Value::Bool(b) => serde_json::Value::Bool(*b),
interp::Value::Str(s) => serde_json::Value::String(s.clone()),
other => serde_json::Value::String(other.display()),
}
}
serde_json::to_string(&to_json(val)).unwrap_or_else(|_| val.display())
}
pub fn evaluate_line(text: &str) -> Result<String, String> {
let mut interp = interp::Interpreter::new();
match interp.eval_expr_str(text) {
@ -251,4 +296,49 @@ mod tests {
assert_eq!(result.results.len(), 1);
assert_eq!(result.results[0].result, "7");
}
#[test]
fn eval_table_format() {
let doc = "let data = [[\"Name\", \"Age\"], [\"Alice\", 30], [\"Bob\", 25]]\n/=| data";
let result = evaluate_document(doc);
assert_eq!(result.results.len(), 1);
assert_eq!(result.results[0].format, "table");
let parsed: Vec<Vec<String>> = serde_json::from_str(&result.results[0].result).unwrap();
assert_eq!(parsed.len(), 3);
assert_eq!(parsed[0], vec!["Name", "Age"]);
}
#[test]
fn eval_tree_format() {
let doc = "let tree = [1, [2, 3], [4, [5]]]\n/=\\ tree";
let result = evaluate_document(doc);
assert_eq!(result.results.len(), 1);
assert_eq!(result.results[0].format, "tree");
let parsed: serde_json::Value = serde_json::from_str(&result.results[0].result).unwrap();
assert!(parsed.is_array());
}
#[test]
fn eval_inline_format_default() {
let doc = "let x = 42\n/= x";
let result = evaluate_document(doc);
assert_eq!(result.results[0].format, "inline");
}
#[test]
fn eval_table_flat_array() {
let doc = "/=| [1, 2, 3]";
let result = evaluate_document(doc);
assert_eq!(result.results.len(), 1);
assert_eq!(result.results[0].format, "table");
}
#[test]
fn eval_document_json_has_format() {
let doc = "let x = 42\n/= x\n/=| [[1, 2], [3, 4]]";
let result = evaluate_document(doc);
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("\"format\":\"inline\""));
assert!(json.contains("\"format\":\"table\""));
}
}

View File

@ -1158,6 +1158,14 @@ fn apply_type_annotation(val: &Value, ann: Option<&str>) -> Result<Value, String
pub struct InterpResult {
pub line: usize,
pub value: Option<Value>,
pub format: EvalFormat,
}
#[derive(Debug, Clone, PartialEq)]
pub enum EvalFormat {
Inline,
Table,
Tree,
}
pub fn interpret_document(lines: &[(usize, &str, bool)]) -> Vec<InterpResult> {
@ -1174,7 +1182,6 @@ pub fn interpret_document(lines: &[(usize, &str, bool)]) -> Vec<InterpResult> {
for &(idx, content, is_eval) in lines {
if is_eval {
// flush any accumulated block first
if !block_acc.is_empty() {
let block_text = block_acc.join("\n");
block_acc.clear();
@ -1182,20 +1189,27 @@ pub fn interpret_document(lines: &[(usize, &str, bool)]) -> Vec<InterpResult> {
match interp.exec_line(&block_text) {
Ok(_) => {}
Err(e) => {
results.push(InterpResult { line: idx, value: Some(Value::Error(e)) });
results.push(InterpResult { line: idx, value: Some(Value::Error(e)), format: EvalFormat::Inline });
continue;
}
}
}
let expr = content.trim().strip_prefix("/=").unwrap_or("").trim();
let trimmed = content.trim();
let (format, expr) = if let Some(rest) = trimmed.strip_prefix("/=|") {
(EvalFormat::Table, rest.trim())
} else if let Some(rest) = trimmed.strip_prefix("/=\\") {
(EvalFormat::Tree, rest.trim())
} else {
(EvalFormat::Inline, trimmed.strip_prefix("/=").unwrap_or("").trim())
};
if expr.is_empty() {
results.push(InterpResult { line: idx, value: Some(Value::Error("empty expression".into())) });
results.push(InterpResult { line: idx, value: Some(Value::Error("empty expression".into())), format });
continue;
}
match interp.eval_expr_str(expr) {
Ok(val) => results.push(InterpResult { line: idx, value: Some(val) }),
Err(e) => results.push(InterpResult { line: idx, value: Some(Value::Error(e)) }),
Ok(val) => results.push(InterpResult { line: idx, value: Some(val), format }),
Err(e) => results.push(InterpResult { line: idx, value: Some(Value::Error(e)), format }),
}
} else {
let trimmed = content.trim();
@ -1211,17 +1225,15 @@ pub fn interpret_document(lines: &[(usize, &str, bool)]) -> Vec<InterpResult> {
block_acc.clear();
brace_depth = 0;
if let Err(e) = interp.exec_line(&block_text) {
results.push(InterpResult { line: idx, value: Some(Value::Error(e)) });
results.push(InterpResult { line: idx, value: Some(Value::Error(e)), format: EvalFormat::Inline });
}
}
} else if opens > closes {
// starting a block
block_acc.push(trimmed.to_string());
brace_depth = opens - closes;
} else {
// single-line cordial statement
if let Err(e) = interp.exec_line(trimmed) {
results.push(InterpResult { line: idx, value: Some(Value::Error(e)) });
results.push(InterpResult { line: idx, value: Some(Value::Error(e)), format: EvalFormat::Inline });
}
}
}

View File

@ -11,7 +11,7 @@ class AppState: ObservableObject {
}
}
}
@Published var evalResults: [Int: String] = [:]
@Published var evalResults: [Int: EvalEntry] = [:]
@Published var noteList: [NoteInfo] = []
@Published var currentNoteID: UUID
@Published var selectedNoteIDs: Set<UUID> = []

View File

@ -916,8 +916,8 @@ struct EditorView: View {
.padding(.top, 4)
}
private func offsetEvalResults(_ results: [Int: String]) -> [Int: String] {
var shifted: [Int: String] = [:]
private func offsetEvalResults(_ results: [Int: EvalEntry]) -> [Int: EvalEntry] {
var shifted: [Int: EvalEntry] = [:]
for (key, val) in results where key > 0 {
shifted[key - 1] = val
}
@ -927,7 +927,7 @@ struct EditorView: View {
struct EditorTextView: NSViewRepresentable {
@Binding var text: String
var evalResults: [Int: String]
var evalResults: [Int: EvalEntry]
var onEvaluate: () -> Void
var onBackspaceAtStart: (() -> Void)? = nil
@ -1579,8 +1579,13 @@ private func highlightCodeLine(_ line: String, lineRange: NSRange, textStorage:
return
}
// Eval prefix
if trimmed.hasPrefix("/=") {
if trimmed.hasPrefix("/=|") || trimmed.hasPrefix("/=\\") {
let prefix = trimmed.hasPrefix("/=|") ? "/=|" : "/=\\"
let prefixRange = (textStorage.string as NSString).range(of: prefix, range: lineRange)
if prefixRange.location != NSNotFound {
textStorage.addAttribute(.foregroundColor, value: syn.operator, range: prefixRange)
}
} else if trimmed.hasPrefix("/=") {
let prefixRange = (textStorage.string as NSString).range(of: "/=", range: lineRange)
if prefixRange.location != NSNotFound {
textStorage.addAttribute(.foregroundColor, value: syn.operator, range: prefixRange)
@ -2201,7 +2206,7 @@ private func resolveLocalImagePath(_ rawPath: String) -> String? {
class LineNumberTextView: NSTextView {
static let gutterWidth: CGFloat = 50
var evalResults: [Int: String] = [:]
var evalResults: [Int: EvalEntry] = [:]
override var textContainerOrigin: NSPoint {
return NSPoint(x: LineNumberTextView.gutterWidth, y: textContainerInset.height)
@ -2302,11 +2307,18 @@ class LineNumberTextView: NSTextView {
numStr.draw(at: NSPoint(x: LineNumberTextView.gutterWidth - numSize.width - 8, y: y))
}
if let result = evalResults[lineNumber - 1] {
let resultStr = NSAttributedString(string: "\u{2192} \(result)", attributes: resultAttrs)
let size = resultStr.size()
let rightEdge = visibleRect.maxX
resultStr.draw(at: NSPoint(x: rightEdge - size.width - 8, y: y))
if let entry = evalResults[lineNumber - 1] {
switch entry.format {
case .table:
drawTableResult(entry.result, at: y, origin: origin, resultAttrs: resultAttrs)
case .tree:
drawTreeResult(entry.result, at: y, origin: origin, resultAttrs: resultAttrs)
case .inline:
let resultStr = NSAttributedString(string: "\u{2192} \(entry.result)", attributes: resultAttrs)
let size = resultStr.size()
let rightEdge = visibleRect.maxX
resultStr.draw(at: NSPoint(x: rightEdge - size.width - 8, y: y))
}
}
lineNumber += 1
@ -2314,6 +2326,165 @@ class LineNumberTextView: NSTextView {
}
}
// MARK: - Table/Tree Rendering
private func drawTableResult(_ json: String, at y: CGFloat, origin: NSPoint, resultAttrs: [NSAttributedString.Key: Any]) {
guard let data = json.data(using: .utf8),
let parsed = try? JSONSerialization.jsonObject(with: data),
let rows = parsed as? [[Any]] else {
let fallback = NSAttributedString(string: "\u{2192} \(json.prefix(40))", attributes: resultAttrs)
let size = fallback.size()
fallback.draw(at: NSPoint(x: visibleRect.maxX - size.width - 8, y: y))
return
}
let palette = Theme.current
let font = Theme.gutterFont
let headerAttrs: [NSAttributedString.Key: Any] = [
.font: NSFontManager.shared.convert(font, toHaveTrait: .boldFontMask),
.foregroundColor: palette.teal
]
let cellAttrs: [NSAttributedString.Key: Any] = [
.font: font,
.foregroundColor: palette.subtext0
]
let borderColor = palette.surface1
let stringRows: [[String]] = rows.map { row in
row.map { cell in
if let s = cell as? String { return s }
if let n = cell as? NSNumber { return "\(n)" }
return "\(cell)"
}
}
guard !stringRows.isEmpty else { return }
let colCount = stringRows.map(\.count).max() ?? 0
guard colCount > 0 else { return }
var colWidths = [CGFloat](repeating: 0, count: colCount)
for row in stringRows {
for (ci, cell) in row.enumerated() where ci < colCount {
let w = (cell as NSString).size(withAttributes: cellAttrs).width
colWidths[ci] = max(colWidths[ci], w)
}
}
let cellPad: CGFloat = 8
let rowHeight: CGFloat = font.pointSize + 6
let tableWidth = colWidths.reduce(0, +) + cellPad * CGFloat(colCount + 1) + CGFloat(colCount - 1)
let tableHeight = rowHeight * CGFloat(stringRows.count) + CGFloat(stringRows.count + 1)
let rightEdge = visibleRect.maxX
let tableX = rightEdge - tableWidth - 12
let tableY = y
let tableRect = NSRect(x: tableX, y: tableY, width: tableWidth, height: tableHeight)
palette.mantle.setFill()
let path = NSBezierPath(roundedRect: tableRect, xRadius: 4, yRadius: 4)
path.fill()
borderColor.setStroke()
path.lineWidth = 0.5
path.stroke()
var cy = tableY + 2
for (ri, row) in stringRows.enumerated() {
let attrs = ri == 0 ? headerAttrs : cellAttrs
var cx = tableX + cellPad
for (ci, cell) in row.enumerated() where ci < colCount {
let str = NSAttributedString(string: cell, attributes: attrs)
str.draw(at: NSPoint(x: cx, y: cy))
cx += colWidths[ci] + cellPad
}
cy += rowHeight
if ri == 0 {
borderColor.setStroke()
let linePath = NSBezierPath()
linePath.move(to: NSPoint(x: tableX + 2, y: cy - 1))
linePath.line(to: NSPoint(x: tableX + tableWidth - 2, y: cy - 1))
linePath.lineWidth = 0.5
linePath.stroke()
}
}
}
private func drawTreeResult(_ json: String, at y: CGFloat, origin: NSPoint, resultAttrs: [NSAttributedString.Key: Any]) {
guard let data = json.data(using: .utf8),
let root = try? JSONSerialization.jsonObject(with: data) else {
let fallback = NSAttributedString(string: "\u{2192} \(json.prefix(40))", attributes: resultAttrs)
let size = fallback.size()
fallback.draw(at: NSPoint(x: visibleRect.maxX - size.width - 8, y: y))
return
}
let palette = Theme.current
let font = Theme.gutterFont
let nodeAttrs: [NSAttributedString.Key: Any] = [
.font: font,
.foregroundColor: palette.teal
]
let branchAttrs: [NSAttributedString.Key: Any] = [
.font: font,
.foregroundColor: palette.overlay0
]
var lines: [(String, Int)] = []
func walk(_ node: Any, depth: Int) {
if let arr = node as? [Any] {
for (i, item) in arr.enumerated() {
let prefix = i == arr.count - 1 ? "\u{2514}" : "\u{251C}"
if item is [Any] {
lines.append(("\(prefix) [\(((item as? [Any])?.count ?? 0))]", depth))
walk(item, depth: depth + 1)
} else {
lines.append(("\(prefix) \(item)", depth))
}
}
} else {
lines.append(("\(node)", depth))
}
}
if let arr = root as? [Any] {
lines.append(("[\(arr.count)]", 0))
walk(root, depth: 1)
} else {
lines.append(("\(root)", 0))
}
let lineHeight = font.pointSize + 4
let indent: CGFloat = 14
var maxWidth: CGFloat = 0
for (text, depth) in lines {
let w = (text as NSString).size(withAttributes: nodeAttrs).width + CGFloat(depth) * indent
maxWidth = max(maxWidth, w)
}
let treeHeight = lineHeight * CGFloat(lines.count) + 4
let treeWidth = maxWidth + 16
let rightEdge = visibleRect.maxX
let treeX = rightEdge - treeWidth - 8
let treeY = y
let treeRect = NSRect(x: treeX, y: treeY, width: treeWidth, height: treeHeight)
palette.mantle.setFill()
let path = NSBezierPath(roundedRect: treeRect, xRadius: 4, yRadius: 4)
path.fill()
palette.surface1.setStroke()
path.lineWidth = 0.5
path.stroke()
var cy = treeY + 2
for (text, depth) in lines {
let x = treeX + 8 + CGFloat(depth) * indent
let attrs = depth == 0 ? nodeAttrs : branchAttrs
let str = NSAttributedString(string: text, attributes: attrs)
str.draw(at: NSPoint(x: x, y: cy))
cy += lineHeight
}
}
// MARK: - Paste (image from clipboard)
override func paste(_ sender: Any?) {

View File

@ -6,6 +6,17 @@ struct NoteInfo: Identifiable {
var lastModified: Date
}
enum EvalFormat: String {
case inline
case table
case tree
}
struct EvalEntry {
let result: String
let format: EvalFormat
}
class RustBridge {
static let shared = RustBridge()
@ -41,7 +52,7 @@ class RustBridge {
return str
}
func evaluate(_ id: UUID) -> [Int: String] {
func evaluate(_ id: UUID) -> [Int: EvalEntry] {
guard let ptr = docs[id] else { return [:] }
guard let cstr = swiftly_doc_evaluate(ptr) else { return [:] }
let json = String(cString: cstr)
@ -118,14 +129,15 @@ class RustBridge {
return str
}
private func parseEvalJSON(_ json: String) -> [Int: String] {
private func parseEvalJSON(_ json: String) -> [Int: EvalEntry] {
guard let data = json.data(using: .utf8) else { return [:] }
guard let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return [:] }
guard let results = obj["results"] as? [[String: Any]] else { return [:] }
var dict: [Int: String] = [:]
var dict: [Int: EvalEntry] = [:]
for item in results {
if let line = item["line"] as? Int, let result = item["result"] as? String {
dict[line] = result
let fmt = EvalFormat(rawValue: item["format"] as? String ?? "inline") ?? .inline
dict[line] = EvalEntry(result: result, format: fmt)
}
}
return dict