A structured-data shell in Gleam, inspired by Nushell
0

Configure Feed

Select the types of activity you want to include in your feed.

gleshell / test / gleshell_test.gleam
46 kB 1380 lines
1import gleam/list 2import gleam/option.{None, Some} 3import gleam/string 4import gleeunit 5import gleshell/builtins 6import gleshell/color 7import gleshell/display 8import gleshell/env 9import gleshell/eval 10import gleshell/highlight 11import gleshell/lexer 12import gleshell/pager 13import gleshell/parser 14import gleshell/syntax 15import gleshell/sys 16import gleshell/value.{Bool, Int, List, Nothing, Record, String, Table} 17import simplifile 18 19pub fn main() -> Nil { 20 gleeunit.main() 21} 22 23// --- lexer --- 24 25pub fn lexer_pipeline_test() { 26 let assert Ok(tokens) = lexer.tokenize("ls | where type == file") 27 let assert [lexer.Ident("ls"), lexer.Pipe, lexer.Ident("where"), ..] = tokens 28 Nil 29} 30 31pub fn lexer_string_and_number_test() { 32 let assert Ok(tokens) = lexer.tokenize("echo \"hi\" 42 3.14 true") 33 let assert [ 34 lexer.Ident("echo"), 35 lexer.StringLit("hi"), 36 lexer.IntLit(42), 37 lexer.FloatLit(3.14), 38 lexer.BoolLit(True), 39 lexer.Eof, 40 ] = tokens 41 Nil 42} 43 44pub fn lexer_path_idents_test() { 45 // Dotfiles, relative/absolute paths, home — must be bare words for `ls .jj` etc. 46 let assert Ok(tokens) = lexer.tokenize("ls .jj ./src ../foo /tmp ~ ~/code") 47 let assert [ 48 lexer.Ident("ls"), 49 lexer.Ident(".jj"), 50 lexer.Ident("./src"), 51 lexer.Ident("../foo"), 52 lexer.Ident("/tmp"), 53 lexer.Ident("~"), 54 lexer.Ident("~/code"), 55 lexer.Eof, 56 ] = tokens 57 Nil 58} 59 60pub fn lexer_bare_double_dash_test() { 61 // POSIX end-of-options: `nix run . -- args` must not lex-error on bare `--`. 62 let assert Ok(tokens) = lexer.tokenize("nix run . -- chadfowler.com yolo") 63 let assert [ 64 lexer.Ident("nix"), 65 lexer.Ident("run"), 66 lexer.Ident("."), 67 lexer.Flag(""), 68 lexer.Ident("chadfowler.com"), 69 lexer.Ident("yolo"), 70 lexer.Eof, 71 ] = tokens 72 // Named long flags still work 73 let assert Ok(tokens2) = lexer.tokenize("cmd --think --scale 3") 74 let assert [ 75 lexer.Ident("cmd"), 76 lexer.Flag("think"), 77 lexer.Flag("scale"), 78 lexer.IntLit(3), 79 lexer.Eof, 80 ] = tokens2 81 Nil 82} 83 84pub fn lexer_flake_ref_hash_test() { 85 // Mid-token `#` is part of the bareword (nix flake refs), not a comment. 86 let assert Ok(tokens) = lexer.tokenize("nix run nixpkgs#hello .#package") 87 let assert [ 88 lexer.Ident("nix"), 89 lexer.Ident("run"), 90 lexer.Ident("nixpkgs#hello"), 91 lexer.Ident(".#package"), 92 lexer.Eof, 93 ] = tokens 94 // Leading `#` at a word boundary still starts a line comment. 95 let assert Ok(tokens2) = lexer.tokenize("echo hi # not parsed") 96 let assert [ 97 lexer.Ident("echo"), 98 lexer.Ident("hi"), 99 lexer.Eof, 100 ] = tokens2 101 Nil 102} 103 104pub fn parse_flake_ref_hash_test() { 105 let assert Ok(parser.Expr(parser.Pipeline([ 106 parser.Command("nix", args, False), 107 ]))) = parser.parse("nix shell nixpkgs#cowsay") 108 let assert [ 109 parser.ValueArg(parser.Lit(String("shell"))), 110 parser.ValueArg(parser.Lit(String("nixpkgs#cowsay"))), 111 ] = args 112 Nil 113} 114 115pub fn parse_bare_double_dash_test() { 116 let assert Ok(parser.Expr(parser.Pipeline([ 117 parser.Command("nix", args, False), 118 ]))) = parser.parse("nix run . -- chadfowler.com yolo") 119 let assert [ 120 parser.ValueArg(parser.Lit(String("run"))), 121 parser.ValueArg(parser.Lit(String("."))), 122 parser.ValueArg(parser.Lit(String("--"))), 123 parser.ValueArg(parser.Lit(String("chadfowler.com"))), 124 parser.ValueArg(parser.Lit(String("yolo"))), 125 ] = args 126 Nil 127} 128 129pub fn parse_ls_dotfile_test() { 130 let assert Ok(parser.Expr(parser.Pipeline([ 131 parser.Command("ls", args, False), 132 ]))) = parser.parse("ls .jj") 133 let assert [parser.ValueArg(parser.Lit(String(".jj")))] = args 134 Nil 135} 136 137pub fn parse_port_spec_arg_test() { 138 // `lsof -i :4004` — colon is a record token but must be a bare argv word here. 139 let assert Ok(parser.Expr(parser.Pipeline([ 140 parser.Command("lsof", args, False), 141 ]))) = parser.parse("lsof -i :4004") 142 let assert [ 143 parser.FlagArg("i", parser.None), 144 parser.ValueArg(parser.Lit(String(":4004"))), 145 ] = args 146 // No space: `lsof -i:4004` 147 let assert Ok(parser.Expr(parser.Pipeline([ 148 parser.Command("lsof", args2, False), 149 ]))) = parser.parse("lsof -i:4004") 150 let assert [ 151 parser.FlagArg("i", parser.None), 152 parser.ValueArg(parser.Lit(String(":4004"))), 153 ] = args2 154 // Glued host:port and URL-shaped words 155 let assert Ok(parser.Expr(parser.Pipeline([ 156 parser.Command("echo", args3, False), 157 ]))) = parser.parse("echo host:4004 http://example.com") 158 let assert [ 159 parser.ValueArg(parser.Lit(String("host:4004"))), 160 parser.ValueArg(parser.Lit(String("http://example.com"))), 161 ] = args3 162 Nil 163} 164 165// --- parser --- 166 167pub fn parse_pipeline_test() { 168 let assert Ok(parser.Expr(parser.Pipeline(cmds))) = 169 parser.parse("ls | first 3") 170 let assert [ 171 parser.Command("ls", [], False), 172 parser.Command("first", args, False), 173 ] = cmds 174 let assert [parser.ValueArg(parser.Lit(Int(3)))] = args 175 Nil 176} 177 178pub fn parse_let_test() { 179 let assert Ok(parser.Let( 180 "x", 181 parser.Pipeline([parser.Command("echo", args, False)]), 182 )) = parser.parse("let x = echo 1") 183 let assert [parser.ValueArg(parser.Lit(Int(1)))] = args 184 Nil 185} 186 187pub fn parse_where_ops_test() { 188 let assert Ok(parser.Expr(parser.Pipeline([ 189 parser.Command("where", args, False), 190 ]))) = parser.parse("where type == file") 191 let assert [ 192 parser.ValueArg(parser.Lit(String("type"))), 193 parser.ValueArg(parser.Lit(String("=="))), 194 parser.ValueArg(parser.Lit(String("file"))), 195 ] = args 196 Nil 197} 198 199pub fn parse_list_and_record_test() { 200 let assert Ok(parser.Expr(parser.Pipeline([ 201 parser.Command("echo", args, False), 202 ]))) = parser.parse("echo [1 2] {a: true}") 203 let assert [ 204 parser.ValueArg(parser.ListExpr([parser.Lit(Int(1)), parser.Lit(Int(2))])), 205 parser.ValueArg(parser.RecordExpr([#("a", parser.Lit(Bool(True)))])), 206 ] = args 207 Nil 208} 209 210// --- value helpers --- 211 212pub fn table_from_records_test() { 213 let rows = [ 214 Record([#("name", String("a")), #("n", Int(1))]), 215 Record([#("name", String("b")), #("n", Int(2))]), 216 ] 217 let assert Table( 218 ["name", "n"], 219 [[String("a"), Int(1)], [String("b"), Int(2)]], 220 ) = value.table_from_records(rows) 221 Nil 222} 223 224// --- eval --- 225 226pub fn eval_echo_and_range_test() { 227 let env = env.new() 228 let assert eval.Continue(_, List([Int(0), Int(1), Int(2)])) = 229 eval.eval_source(env, "range 3") 230 let assert eval.Continue(_, String("hello")) = 231 eval.eval_source(env, "echo hello") 232 Nil 233} 234 235pub fn eval_pipeline_reverse_first_test() { 236 let env = env.new() 237 let assert eval.Continue(_, Int(2)) = 238 eval.eval_source(env, "range 3 | reverse | first") 239 Nil 240} 241 242/// Pipeline input must become the external's stdin (`cat f | less`, `echo hi | wc`). 243/// Use `let` so the last stage is capture mode even on a TTY (bare expressions 244/// may inherit the terminal and return an empty string value). 245pub fn eval_pipeline_stdin_to_external_test() { 246 let env = env.new() 247 // `echo` is a builtin; `wc` is external. Count bytes of "hello" (no trailing NL). 248 let assert eval.Continue(_, String(out)) = 249 eval.eval_source(env, "let n = echo hello | ^wc -c") 250 let assert True = string.contains(out, "5") 251 // Tight `cmd|cmd` form (no spaces around pipe) 252 let assert eval.Continue(_, String(out2)) = 253 eval.eval_source(env, "let m = echo ab|^wc -c") 254 let assert True = string.contains(out2, "2") 255 Nil 256} 257 258pub fn eval_let_and_var_test() { 259 let env = env.new() 260 let assert eval.Continue(env2, Int(7)) = 261 eval.eval_source(env, "let n = echo 7") 262 let assert eval.Continue(_, Int(7)) = eval.eval_source(env2, "echo $n") 263 Nil 264} 265 266pub fn eval_env_var_get_test() { 267 // `$env.HOME` and bare `$env.HOME` as pipeline source 268 let env = env.new() 269 let assert Ok(home) = sys.getenv("HOME") 270 let assert eval.Continue(_, String(got)) = 271 eval.eval_source(env, "echo $env.HOME") 272 let assert True = got == home 273 let assert eval.Continue(_, String(got2)) = eval.eval_source(env, "$env.HOME") 274 let assert True = got2 == home 275 Nil 276} 277 278pub fn eval_env_record_test() { 279 let env = env.new() 280 let assert eval.Continue(_, Record(fields)) = eval.eval_source(env, "$env") 281 let assert True = list_has_string_field(fields, "HOME") 282 let assert True = list_has_string_field(fields, "PATH") 283 // `$env | get HOME` 284 let assert eval.Continue(_, String(home)) = 285 eval.eval_source(env, "$env | get HOME") 286 let assert True = string.length(home) > 0 287 Nil 288} 289 290pub fn eval_env_assign_test() { 291 let env = env.new() 292 let assert eval.Continue(env2, String("gleshell-test-val")) = 293 eval.eval_source(env, "$env.GLESHELL_TEST_VAR = gleshell-test-val") 294 let assert eval.Continue(_, String("gleshell-test-val")) = 295 eval.eval_source(env2, "$env.GLESHELL_TEST_VAR") 296 let assert Ok("gleshell-test-val") = sys.getenv("GLESHELL_TEST_VAR") 297 Nil 298} 299 300pub fn parse_env_assign_test() { 301 let assert Ok(parser.EnvAssign("FOO", parser.Pipeline([cmd]))) = 302 parser.parse("$env.FOO = hello") 303 let assert parser.Command( 304 "__value__", 305 [parser.ValueArg(parser.Lit(String("hello")))], 306 False, 307 ) = cmd 308 Nil 309} 310 311fn list_has_string_field( 312 fields: List(#(String, value.Value)), 313 key: String, 314) -> Bool { 315 case fields { 316 [] -> False 317 [#(k, String(_)), ..] if k == key -> True 318 [_, ..rest] -> list_has_string_field(rest, key) 319 } 320} 321 322pub fn eval_where_select_test() { 323 let env = env.new() 324 // build table via records in a list, convert with table 325 let assert eval.Continue(_, result) = 326 eval.eval_source( 327 env, 328 "echo [{name: a, n: 1} {name: b, n: 2} {name: c, n: 3}] | table | where n > 1 | select name", 329 ) 330 let assert Table(["name"], rows) = result 331 let assert [[String("b")], [String("c")]] = rows 332 Nil 333} 334 335pub fn about_command_test() { 336 let env = env.new() 337 let assert eval.Continue(_, String(text)) = eval.eval_source(env, "about") 338 let assert True = string.contains(text, "gleshell") 339 let assert True = string.contains(text, "nandi.uk") 340 let assert True = string.contains(text, "NaNdi") 341 let assert True = string.contains(text, "did:plc:ngokl2gnmpbvuvrfckja3g7p") 342 let assert True = string.contains(text, "latha.org") 343 let assert eval.Continue(_, String(which_out)) = 344 eval.eval_source(env, "which about") 345 let assert "builtin: about" = which_out 346 Nil 347} 348 349pub fn help_covers_all_builtins_test() { 350 // Every registered builtin must have a dedicated help_text entry. 351 let assert [] = builtins.missing_help() 352 353 let env = env.new() 354 // which finds builtins; help must too (not "unknown command") 355 let assert eval.Continue(_, String(which_out)) = 356 eval.eval_source(env, "which table") 357 let assert "builtin: table" = which_out 358 let assert eval.Continue(_, String(help_out)) = 359 eval.eval_source(env, "help table") 360 let assert True = string.contains(help_out, "table") 361 let assert True = string.contains(help_out, "coerce") 362 363 // Parent commands with subcommands: `help to` / `help from` / `help http`. 364 let assert eval.Continue(_, String(to_help)) = 365 eval.eval_source(env, "help to") 366 let assert True = string.contains(to_help, "json") 367 let assert eval.Continue(_, String(from_help)) = 368 eval.eval_source(env, "help from") 369 let assert True = string.contains(from_help, "json") 370 let assert eval.Continue(_, String(http_help)) = 371 eval.eval_source(env, "help http") 372 let assert True = string.contains(http_help, "get") 373 let assert True = string.contains(http_help, "post") 374 375 // Bare help lists every command with its one-line description. 376 let assert eval.Continue(_, String(all)) = eval.eval_source(env, "help") 377 list.each(builtins.names(), fn(name) { 378 let assert True = string.contains(all, name) 379 }) 380 381 let assert eval.Continue(env2, value.Fail(msg)) = 382 eval.eval_source(env, "help not-a-real-cmd") 383 let assert True = string.contains(msg, "unknown command") 384 let assert 1 = env2.last_exit 385 Nil 386} 387 388pub fn eval_from_json_test() { 389 let env = env.new() 390 let assert eval.Continue(_, Record(fields)) = 391 eval.eval_source(env, "echo \"{\\\"x\\\": 1}\" | from json") 392 let assert True = list_has_field(fields, "x", Int(1)) 393 Nil 394} 395 396pub fn eval_to_json_pretty_test() { 397 let env = env.new() 398 // `to` command + `json` subcommand — pretty by default 399 let assert eval.Continue(_, String(pretty)) = 400 eval.eval_source(env, "echo [1 2 3] | to json") 401 let assert True = string.contains(pretty, "\n") 402 let assert True = string.contains(pretty, "1") 403 // `--raw` matches Nu: compact, no trailing newline 404 let assert eval.Continue(_, String(raw)) = 405 eval.eval_source(env, "echo [1 2 3] | to json --raw") 406 let assert "[1,2,3]" = raw 407 // Missing / unknown subcommand 408 let assert eval.Continue(env2, value.Fail(msg)) = 409 eval.eval_source(env, "echo 1 | to") 410 let assert True = string.contains(msg, "subcommand") 411 let assert 1 = env2.last_exit 412 let assert eval.Continue(_, value.Fail(msg2)) = 413 eval.eval_source(env, "echo 1 | to yaml") 414 let assert True = string.contains(msg2, "unknown subcommand") 415 Nil 416} 417 418pub fn eval_to_json_record_test() { 419 let env = env.new() 420 let assert eval.Continue(_, String(raw)) = 421 eval.eval_source(env, "echo {a: 1, b: true} | to json -r") 422 let assert True = string.contains(raw, "\"a\":1") 423 let assert True = string.contains(raw, "\"b\":true") 424 Nil 425} 426 427pub fn http_subcommand_errors_test() { 428 let env = env.new() 429 // Missing subcommand 430 let assert eval.Continue(env2, value.Fail(msg)) = 431 eval.eval_source(env, "http") 432 let assert True = string.contains(msg, "subcommand") 433 let assert 1 = env2.last_exit 434 // Unknown subcommand 435 let assert eval.Continue(_, value.Fail(msg2)) = 436 eval.eval_source(env, "http foo") 437 let assert True = string.contains(msg2, "unknown subcommand") 438 // Missing URL 439 let assert eval.Continue(_, value.Fail(msg3)) = 440 eval.eval_source(env, "http get") 441 let assert True = string.contains(msg3, "URL") || string.contains(msg3, "url") 442 // Invalid URL 443 let assert eval.Continue(_, value.Fail(msg4)) = 444 eval.eval_source(env, "http get not-a-url") 445 let assert True = string.contains(msg4, "invalid URL") 446 // which / help 447 let assert eval.Continue(_, String(which_out)) = 448 eval.eval_source(env, "which http") 449 let assert "builtin: http" = which_out 450 Nil 451} 452 453pub fn http_get_live_test() { 454 // Live request against postman-echo (JSON). Skip gracefully if offline. 455 let env = env.new() 456 case eval.eval_source(env, "http get --full https://postman-echo.com/get") { 457 eval.Continue(_, Record(fields)) -> { 458 let assert True = list_has_field(fields, "status", Int(200)) 459 let assert True = list_has_key(fields, "body") 460 let assert True = list_has_key(fields, "headers") 461 let assert True = 462 list_has_field(fields, "url", String("https://postman-echo.com/get")) 463 // Default path (no --full) parses JSON body into a record 464 let assert eval.Continue(_, Record(body_fields)) = 465 eval.eval_source(env, "http get https://postman-echo.com/get") 466 let assert True = list_has_key(body_fields, "url") 467 Nil 468 } 469 eval.Continue(_, value.Fail(msg)) -> { 470 // Network unavailable — still assert the error is from http, not parse 471 let assert True = 472 string.contains(msg, "http:") || string.contains(msg, "failed") 473 Nil 474 } 475 _ -> panic as "http get --full: unexpected eval result" 476 } 477} 478 479pub fn http_post_json_live_test() { 480 let env = env.new() 481 case 482 eval.eval_source( 483 env, 484 "http post --full https://postman-echo.com/post {name: gleshell}", 485 ) 486 { 487 eval.Continue(_, Record(fields)) -> { 488 let assert True = list_has_field(fields, "status", Int(200)) 489 // JSON body should be parsed; postman-echo echoes under `json` 490 case list_find_field(fields, "body") { 491 Ok(Record(body)) -> { 492 case list_find_field(body, "json") { 493 Ok(Record(json_fields)) -> { 494 let assert True = 495 list_has_field(json_fields, "name", String("gleshell")) 496 Nil 497 } 498 _ -> Nil 499 } 500 } 501 _ -> Nil 502 } 503 } 504 eval.Continue(_, value.Fail(_)) -> Nil 505 _ -> panic as "http post --full: unexpected eval result" 506 } 507} 508 509fn list_has_key(fields: List(#(String, value.Value)), key: String) -> Bool { 510 case fields { 511 [] -> False 512 [#(k, _), ..rest] -> 513 case k == key { 514 True -> True 515 False -> list_has_key(rest, key) 516 } 517 } 518} 519 520fn list_find_field( 521 fields: List(#(String, value.Value)), 522 key: String, 523) -> Result(value.Value, Nil) { 524 case fields { 525 [] -> Error(Nil) 526 [#(k, v), ..rest] -> 527 case k == key { 528 True -> Ok(v) 529 False -> list_find_field(rest, key) 530 } 531 } 532} 533 534fn list_has_field( 535 fields: List(#(String, value.Value)), 536 key: String, 537 expected: value.Value, 538) -> Bool { 539 case fields { 540 [] -> False 541 [#(k, v), ..rest] -> 542 case k == key { 543 True -> value.equals(v, expected) 544 False -> list_has_field(rest, key, expected) 545 } 546 } 547} 548 549pub fn eval_length_test() { 550 let env = env.new() 551 let assert eval.Continue(_, Int(4)) = 552 eval.eval_source(env, "range 4 | length") 553 Nil 554} 555 556pub fn eval_which_builtin_test() { 557 let env = env.new() 558 let assert eval.Continue(_, String("builtin: ls")) = 559 eval.eval_source(env, "which ls") 560 Nil 561} 562 563pub fn eval_find_list_test() { 564 let env = env.new() 565 // Substring match (OR across terms) 566 let assert eval.Continue(_, List(items)) = 567 eval.eval_source(env, "echo [moe larry curly] | find l") 568 let assert [String("larry"), String("curly")] = items 569 // Multiple terms 570 let assert eval.Continue(_, List(items2)) = 571 eval.eval_source(env, "echo [a.toml b.md c.rs] | find toml md") 572 let assert [String("a.toml"), String("b.md")] = items2 573 // Exact number match 574 let assert eval.Continue(_, List([Int(5)])) = 575 eval.eval_source(env, "echo [1 5 3 4 35] | find 5") 576 Nil 577} 578 579pub fn eval_find_ignore_case_invert_test() { 580 let env = env.new() 581 let assert eval.Continue(_, List(items)) = 582 eval.eval_source(env, "echo [Hello world HELLO] | find hello -i") 583 let assert [String("Hello"), String("HELLO")] = items 584 // `-i term` may attach term to the flag; still works 585 let assert eval.Continue(_, List(items2)) = 586 eval.eval_source(env, "echo [Hello world HELLO] | find -i hello") 587 let assert [String("Hello"), String("HELLO")] = items2 588 let assert eval.Continue(_, List([String("cd")])) = 589 eval.eval_source(env, "echo [ab cd] | find --invert a") 590 Nil 591} 592 593pub fn eval_find_table_and_string_test() { 594 let env = env.new() 595 let assert eval.Continue(_, Table(["name", "type"], rows)) = 596 eval.eval_source( 597 env, 598 "echo [{name: a, type: file} {name: b, type: dir}] | table | find file", 599 ) 600 let assert [[String("a"), String("file")]] = rows 601 // Single-line string: return the string if match, else nothing 602 let assert eval.Continue(_, String("Cargo.toml")) = 603 eval.eval_source(env, "echo Cargo.toml | find Cargo") 604 let assert eval.Continue(_, Nothing) = 605 eval.eval_source(env, "echo Cargo.toml | find zz") 606 // Multi-line string → list of matching lines 607 let assert eval.Continue(_, List(lines)) = 608 eval.eval_source(env, "echo \"hi\nbye\nhi there\" | find hi") 609 let assert [String("hi"), String("hi there")] = lines 610 Nil 611} 612 613pub fn eval_find_regex_test() { 614 let env = env.new() 615 let assert eval.Continue(_, List(items)) = 616 eval.eval_source(env, "echo [abc odb arc abf] | find --regex \"b.\"") 617 let assert [String("abc"), String("abf")] = items 618 // Regex + terms is rejected (Nu-compatible) 619 let assert eval.Continue(env2, value.Fail(msg)) = 620 eval.eval_source(env, "echo [abc] | find --regex \"b.\" x") 621 let assert True = string.contains(msg, "regex") 622 let assert 1 = env2.last_exit 623 Nil 624} 625 626pub fn eval_which_all_test() { 627 let env = env.new() 628 // `-a` includes the builtin first, then any PATH copies of `ls`. 629 let assert eval.Continue(_, result) = eval.eval_source(env, "which -a ls") 630 case result { 631 String("builtin: ls") -> Nil 632 List([String("builtin: ls"), ..]) -> Nil 633 other -> { 634 let _ = other 635 panic as "which -a ls should start with builtin: ls" 636 } 637 } 638} 639 640pub fn eval_which_external_test() { 641 let env = env.new() 642 // `sh` is not a gleshell builtin; should resolve via PATH. 643 let assert eval.Continue(_, String(path)) = eval.eval_source(env, "which sh") 644 let assert True = string.contains(path, "sh") 645 Nil 646} 647 648pub fn eval_which_follow_test() { 649 let env = env.new() 650 // `-f` follows symlinks to a canonical path (NixOS: sh → bash store path). 651 let assert eval.Continue(_, String(plain)) = eval.eval_source(env, "which sh") 652 let assert eval.Continue(_, String(followed)) = 653 eval.eval_source(env, "which -f sh") 654 // Followed path is absolute and exists; if plain was a symlink, they differ. 655 let assert True = string.starts_with(followed, "/") 656 let assert True = string.length(followed) > 0 657 // Combined with -a: first entry for a non-builtin is still a real path. 658 let assert eval.Continue(_, result) = eval.eval_source(env, "which -a -f sh") 659 case result { 660 String(p) -> { 661 let assert True = string.starts_with(p, "/") 662 Nil 663 } 664 List([String(p), ..]) -> { 665 let assert True = string.starts_with(p, "/") 666 Nil 667 } 668 other -> { 669 let _ = other 670 panic as "which -a -f sh should yield a path" 671 } 672 } 673 let _ = plain 674 let _ = followed 675 Nil 676} 677 678pub fn value_nothing_falsey_test() { 679 let assert False = value.is_truthy(Nothing) 680 let assert True = value.is_truthy(Int(1)) 681 Nil 682} 683 684// --- display / color --- 685 686pub fn display_plain_no_ansi_test() { 687 let text = display.render_with(False, Bool(True)) 688 let assert "true" = text 689 let assert False = string_contains(text, "\u{001b}") 690 Nil 691} 692 693pub fn display_colored_has_ansi_test() { 694 let text = display.render_with(True, Bool(True)) 695 let assert True = string_contains(text, "\u{001b}") 696 let assert True = string_contains(text, "true") 697 Nil 698} 699 700pub fn display_preserves_external_ansi_test() { 701 // Pre-colored tool output (jj, git, …) must not be re-wrapped in string green. 702 let app = "\u{001b}[1;31merror\u{001b}[0m: boom" 703 let text = display.render_with(True, String(app)) 704 let assert True = text == app 705 Nil 706} 707 708pub fn display_multiline_string_not_recolored_test() { 709 let multi = "line1\nline2" 710 let text = display.render_with(True, String(multi)) 711 let assert True = text == multi 712 Nil 713} 714 715pub fn display_table_headers_colored_test() { 716 let text = 717 display.render_with( 718 True, 719 Table(["name", "type"], [[String("src"), String("dir")]]), 720 ) 721 // bold green header + bright-blue dir name 722 let assert True = string_contains(text, "\u{001b}[1;32m") 723 let assert True = string_contains(text, "\u{001b}[94m") 724 let assert True = string_contains(text, "src") 725 Nil 726} 727 728pub fn format_filesize_units_test() { 729 let assert "0 B" = display.format_filesize(0) 730 let assert "512 B" = display.format_filesize(512) 731 let assert "1023 B" = display.format_filesize(1023) 732 let assert "1 KB" = display.format_filesize(1024) 733 let assert "1.5 KB" = display.format_filesize(1536) 734 let assert "1 MB" = display.format_filesize(1_048_576) 735 let assert "1.5 MB" = display.format_filesize(1_048_576 + 524_288) 736 let assert "1 GB" = display.format_filesize(1_073_741_824) 737 Nil 738} 739 740pub fn display_size_column_humanized_test() { 741 // Data stays as raw bytes (Int); only display shows KB/MB. 742 let text = 743 display.render_with( 744 False, 745 Table(["name", "size"], [[String("a"), Int(2048)]]), 746 ) 747 let assert True = string_contains(text, "2 KB") 748 let assert False = string_contains(text, "2048") 749 Nil 750} 751 752pub fn format_datetime_shape_test() { 753 // Local 12-hour with abbr month: "Nov 14 2023 2:13:20 PM" (example shape). 754 let text = display.format_datetime(1_700_000_000) 755 let assert True = string_contains(text, ":") 756 let assert True = 757 string_contains(text, " AM") || string_contains(text, " PM") 758 let assert True = 759 string_contains(text, "Jan") 760 || string_contains(text, "Feb") 761 || string_contains(text, "Mar") 762 || string_contains(text, "Apr") 763 || string_contains(text, "May") 764 || string_contains(text, "Jun") 765 || string_contains(text, "Jul") 766 || string_contains(text, "Aug") 767 || string_contains(text, "Sep") 768 || string_contains(text, "Oct") 769 || string_contains(text, "Nov") 770 || string_contains(text, "Dec") 771 Nil 772} 773 774pub fn display_modified_column_humanized_test() { 775 // Data stays as Unix seconds; only display shows a local 12-hour datetime. 776 let text = 777 display.render_with( 778 False, 779 Table(["name", "modified"], [[String("a"), Int(1_700_000_000)]]), 780 ) 781 let assert False = string_contains(text, "1700000000") 782 let assert True = string_contains(text, ":") 783 let assert True = 784 string_contains(text, " AM") || string_contains(text, " PM") 785 Nil 786} 787 788pub fn ls_includes_modified_column_test() { 789 let env = env.new() 790 // `first` collapses a single row to a record. 791 let assert eval.Continue(_, Record(fields)) = 792 eval.eval_source(env, "ls | first 1") 793 let assert True = list_has_key(fields, "name") 794 let assert True = list_has_key(fields, "type") 795 let assert True = list_has_key(fields, "size") 796 let assert True = list_has_key(fields, "modified") 797 // Pipeline data stays as Int (epoch seconds); display formats it. 798 case list_find_field(fields, "modified") { 799 Ok(Int(n)) if n >= 0 -> Nil 800 _ -> panic as "ls modified should be non-negative Unix epoch seconds" 801 } 802} 803 804pub fn color_visible_length_strips_ansi_test() { 805 let painted = color.paint(True, "\u{001b}[32m", "hi") 806 let assert 2 = color.visible_length(painted) 807 let assert 2 = color.visible_length("hi") 808 Nil 809} 810 811// --- less / pager --- 812 813pub fn pager_wrap_respects_ansi_width_test() { 814 // 10 visible chars of content; wrap at 4 → three physical lines. 815 let painted = color.paint(True, "\u{001b}[32m", "abcdefghij") 816 let lines = pager.wrap_line(painted, 4) 817 let assert 3 = list.length(lines) 818 // Each physical line still carries / continues color; visible width ≤ 4. 819 list.each(lines, fn(line) { 820 let assert True = color.visible_length(line) <= 4 821 }) 822 // Joining without separators reconstructs the original SGR + text. 823 let joined = string.join(lines, "") 824 let assert True = string.contains(joined, "abcdefghij") 825 let assert True = string.contains(joined, "\u{001b}[32m") 826 Nil 827} 828 829pub fn pager_display_lines_splits_newlines_test() { 830 let lines = pager.display_lines("a\nb\nc", 80) 831 let assert ["a", "b", "c"] = lines 832 Nil 833} 834 835pub fn strip_ansi_removes_csi_test() { 836 let painted = color.paint(True, "\u{001b}[32m", "hello") 837 let assert "hello" = color.strip_ansi(painted) 838 let assert "plain" = color.strip_ansi("plain") 839 let assert "ab" = color.strip_ansi("a\u{001b}[1;31mb\u{001b}[0m") 840 Nil 841} 842 843pub fn pager_line_matches_strips_ansi_test() { 844 let painted = color.paint(True, "\u{001b}[31m", "needle") 845 let assert True = pager.line_matches(painted, "needle") 846 let assert True = pager.line_matches(painted, "eed") 847 // Case-insensitive: mixed / upper pattern still hits. 848 let assert True = pager.line_matches(painted, "NEEDLE") 849 let assert True = pager.line_matches(painted, "NeEd") 850 let assert False = pager.line_matches(painted, "") 851 // Pattern must not match inside CSI itself. 852 let assert False = pager.line_matches(painted, "[31m") 853 Nil 854} 855 856pub fn pager_find_after_and_before_test() { 857 let lines = ["alpha", "bravo", "alpha", "charlie"] 858 let assert Ok(#(0, False)) = pager.find_after(lines, "alpha", -1) 859 let assert Ok(#(2, False)) = pager.find_after(lines, "alpha", 0) 860 // Wrap from end back to first match. 861 let assert Ok(#(0, True)) = pager.find_after(lines, "alpha", 2) 862 let assert Error(Nil) = pager.find_after(lines, "zzz", -1) 863 // Case-insensitive search. 864 let assert Ok(#(0, False)) = pager.find_after(lines, "ALPHA", -1) 865 let assert Ok(#(2, False)) = pager.find_after(lines, "AlPhA", 0) 866 867 let assert Ok(#(2, False)) = pager.find_before(lines, "alpha", 3) 868 let assert Ok(#(0, False)) = pager.find_before(lines, "alpha", 2) 869 // Wrap from start back to last match. 870 let assert Ok(#(2, True)) = pager.find_before(lines, "alpha", 0) 871 let assert Ok(#(2, False)) = pager.find_before(lines, "ALPHA", 3) 872 Nil 873} 874 875pub fn pager_find_empty_pattern_test() { 876 let lines = ["a", "b"] 877 let assert Error(Nil) = pager.find_after(lines, "", -1) 878 let assert Error(Nil) = pager.find_before(lines, "", 2) 879 Nil 880} 881 882pub fn pager_live_search_preview_test() { 883 let lines = ["alpha", "bravo", "charlie", "alpha again"] 884 // Empty query: stay put, no highlight. 885 let assert #(2, None, None) = pager.live_search_preview(lines, "", 2) 886 // Inclusive of start_offset (match on current top line). 887 let assert #(0, Some("alpha"), None) = 888 pager.live_search_preview(lines, "alpha", 0) 889 // From mid-buffer: first match at or after start. 890 let assert #(3, Some("alpha"), None) = 891 pager.live_search_preview(lines, "alpha", 1) 892 // Not found: keep start offset, still paint pattern, status suffix. 893 let assert #(1, Some("zzz"), Some("not found")) = 894 pager.live_search_preview(lines, "zzz", 1) 895 // Progressive typing narrows: "ch" → charlie. 896 let assert #(2, Some("ch"), None) = pager.live_search_preview(lines, "ch", 0) 897 // Case-insensitive: upper pattern still finds lower content. 898 let assert #(0, Some("ALPHA"), None) = 899 pager.live_search_preview(lines, "ALPHA", 0) 900 let assert #(3, Some("Alpha"), None) = 901 pager.live_search_preview(lines, "Alpha", 1) 902 Nil 903} 904 905pub fn pager_highlight_matches_test() { 906 let out = pager.highlight_matches("hello world", "world") 907 // Black on bright yellow accent. 908 let assert True = string.contains(out, "\u{001b}[30;103m") 909 let assert True = string.contains(out, "world") 910 let assert True = string.contains(out, "\u{001b}[39;49m") 911 let assert "hello world" = color.strip_ansi(out) 912 913 // Case-insensitive highlight preserves original casing in the line. 914 let ci = pager.highlight_matches("Hello World", "WORLD") 915 let assert True = string.contains(ci, "\u{001b}[30;103m") 916 let assert True = string.contains(ci, "World") 917 let assert "Hello World" = color.strip_ansi(ci) 918 919 // No match / empty pattern: unchanged. 920 let assert "nope" = pager.highlight_matches("nope", "zzz") 921 let assert "x" = pager.highlight_matches("x", "") 922 923 // Multiple non-overlapping hits. 924 let multi = pager.highlight_matches("aa x aa", "aa") 925 let parts = string.split(multi, "\u{001b}[30;103m") 926 let assert 3 = list.length(parts) 927 928 // ANSI around the match is preserved; highlight still finds visible text. 929 let painted = color.paint(True, "\u{001b}[32m", "needle here") 930 let hi = pager.highlight_matches(painted, "needle") 931 let assert True = string.contains(hi, "\u{001b}[32m") 932 let assert True = string.contains(hi, "\u{001b}[30;103m") 933 let assert "needle here" = color.strip_ansi(hi) 934 let assert True = string.contains(hi, "\u{001b}[39;49m") 935 Nil 936} 937 938pub fn pager_highlight_match_spanning_sgr_test() { 939 // Match crosses a mid-string color change: black-on-yellow re-opens after SGR. 940 let line = "ab\u{001b}[31mcd\u{001b}[0mef" 941 let hi = pager.highlight_matches(line, "bcde") 942 let assert True = string.contains(hi, "\u{001b}[30;103m") 943 let assert "abcdef" = color.strip_ansi(hi) 944 // After the red open, accent should be re-applied inside the match. 945 let assert True = string.contains(hi, "\u{001b}[31m\u{001b}[30;103m") 946 Nil 947} 948 949pub fn less_short_output_passthrough_test() { 950 // Non-TTY test runner: needs_paging is false → less returns the text. 951 let env = env.new() 952 let assert eval.Continue(_, String(out)) = 953 eval.eval_source(env, "echo hello | less") 954 let assert True = string.contains(out, "hello") 955 let assert eval.Continue(_, String(which_out)) = 956 eval.eval_source(env, "which less") 957 let assert "builtin: less" = which_out 958 Nil 959} 960 961pub fn less_preserves_ansi_in_string_test() { 962 let env = env.new() 963 // Pre-colored multi-line text must survive less (no re-paint / strip). 964 let colored = "\u{001b}[31mred\u{001b}[0m\n\u{001b}[32mgreen\u{001b}[0m" 965 let assert eval.Continue(_, String(out)) = 966 eval.eval_source(env, "echo \"" <> escape_for_source(colored) <> "\" | less") 967 let assert True = string.contains(out, "\u{001b}[31m") 968 let assert True = string.contains(out, "red") 969 let assert True = string.contains(out, "\u{001b}[32m") 970 let assert True = string.contains(out, "green") 971 Nil 972} 973 974pub fn less_no_input_errors_test() { 975 let env = env.new() 976 let assert eval.Continue(env2, value.Fail(msg)) = eval.eval_source(env, "less") 977 let assert True = string.contains(msg, "no input") 978 let assert 1 = env2.last_exit 979 Nil 980} 981 982pub fn less_help_test() { 983 let env = env.new() 984 let assert eval.Continue(_, String(help_out)) = 985 eval.eval_source(env, "help less") 986 let assert True = string.contains(help_out, "ANSI") 987 let assert True = string.contains(help_out, "q") 988 let assert True = string.contains(help_out, "/pattern") 989 let assert True = string.contains(help_out, "n / N") 990 Nil 991} 992 993pub fn git_log_pipeline_emits_ansi_and_decorate_test() { 994 // Capture uses a throwaway PTY when color is wanted, so git colorizes and 995 // keeps ref decorations without GIT_CONFIG_* hacks. FORCE_COLOR makes 996 // want_child_color true in non-TTY test runners. 997 let prev = sys.getenv("FORCE_COLOR") 998 let assert Ok(_) = sys.setenv("FORCE_COLOR", "1") 999 let env = env.new() 1000 // Full log (not --oneline): decorations appear on the commit line. 1001 let result = eval.eval_source(env, "git log -1 | identity") 1002 case prev { 1003 Ok(v) -> { 1004 let assert Ok(_) = sys.setenv("FORCE_COLOR", v) 1005 Nil 1006 } 1007 Error(Nil) -> { 1008 let assert Ok(_) = sys.setenv("FORCE_COLOR", "") 1009 Nil 1010 } 1011 } 1012 let assert eval.Continue(_, String(out)) = result 1013 // Real git colors, not gleshell string-green on plain text. 1014 let assert True = string.contains(out, "\u{001b}[") 1015 // TTY-style decorate: ref names like HEAD / main. 1016 let assert True = string.contains(out, "HEAD") || string.contains(out, "main") 1017 Nil 1018} 1019 1020pub fn jj_log_pipeline_emits_ansi_test() { 1021 // jj ignores FORCE_COLOR; PTY capture is what keeps colors for `jj log | less`. 1022 let prev = sys.getenv("FORCE_COLOR") 1023 let assert Ok(_) = sys.setenv("FORCE_COLOR", "1") 1024 let env = env.new() 1025 let result = eval.eval_source(env, "jj log -n 1 | identity") 1026 case prev { 1027 Ok(v) -> { 1028 let assert Ok(_) = sys.setenv("FORCE_COLOR", v) 1029 Nil 1030 } 1031 Error(Nil) -> { 1032 let assert Ok(_) = sys.setenv("FORCE_COLOR", "") 1033 Nil 1034 } 1035 } 1036 let assert eval.Continue(_, String(out)) = result 1037 let assert True = string.contains(out, "\u{001b}[") 1038 Nil 1039} 1040 1041/// Embed a string in double-quoted source: escape `\` and `"`. 1042fn escape_for_source(s: String) -> String { 1043 s 1044 |> string.replace("\\", "\\\\") 1045 |> string.replace("\"", "\\\"") 1046} 1047 1048// --- input syntax highlighting --- 1049 1050pub fn highlight_plain_when_off_test() { 1051 let src = "ls | where type == file" 1052 let assert True = highlight.highlight(False, src) == src 1053 Nil 1054} 1055 1056pub fn highlight_pipeline_has_shapes_test() { 1057 let text = highlight.highlight(True, "ls | first 3") 1058 // bold cyan internalcall, bold purple pipe, bold purple int 1059 let assert True = string_contains(text, "\u{001b}[1;36m") 1060 let assert True = string_contains(text, "\u{001b}[1;35m") 1061 let assert True = string_contains(text, "ls") 1062 let assert True = string_contains(text, "first") 1063 let assert True = string_contains(text, "3") 1064 // visible text unchanged 1065 let assert 12 = color.visible_length(text) 1066 Nil 1067} 1068 1069pub fn highlight_string_and_flag_test() { 1070 let text = highlight.highlight(True, "echo \"hi\" --raw") 1071 let assert True = string_contains(text, "\u{001b}[32m") 1072 let assert True = string_contains(text, "\u{001b}[1;34m") 1073 let assert True = string_contains(text, "hi") 1074 let assert True = string_contains(text, "--raw") 1075 Nil 1076} 1077 1078pub fn highlight_variable_and_let_test() { 1079 let text = highlight.highlight(True, "let x = $in") 1080 let assert True = string_contains(text, "\u{001b}[1;36m") 1081 let assert True = string_contains(text, "\u{001b}[35m") 1082 let assert True = string_contains(text, "let") 1083 let assert True = string_contains(text, "$in") 1084 Nil 1085} 1086 1087pub fn highlight_incomplete_string_test() { 1088 // Unterminated string while typing should not crash 1089 let text = highlight.highlight(True, "echo \"hel") 1090 let assert True = string_contains(text, "hel") 1091 let assert True = string_contains(text, "\u{001b}[32m") 1092 Nil 1093} 1094 1095pub fn highlight_to_command_test() { 1096 // `to` is a builtin; `json` is a subcommand arg 1097 let text = highlight.highlight(True, "range 3 | to json") 1098 let assert True = string_contains(text, "to") 1099 let assert True = string_contains(text, "json") 1100 let assert True = string_contains(text, "\u{001b}[1;36m") 1101 Nil 1102} 1103 1104pub fn highlight_path_arg_not_garbage_test() { 1105 // Dotfile paths must not use shape_garbage (white-on-red) 1106 let text = highlight.highlight(True, "ls .jj ./src /tmp ~/code") 1107 let assert True = string_contains(text, ".jj") 1108 let assert False = string_contains(text, "\u{001b}[1;37;41m") 1109 // args use shape_externalarg (bold green) 1110 let assert True = string_contains(text, "\u{001b}[1;32m") 1111 Nil 1112} 1113 1114fn string_contains(haystack: String, needle: String) -> Bool { 1115 case string.split(haystack, needle) { 1116 [_] -> False 1117 _ -> True 1118 } 1119} 1120 1121// --- file syntax (cat highlighters) --- 1122 1123pub fn syntax_language_from_path_test() { 1124 let assert syntax.Json = syntax.language_from_path("data/foo.json") 1125 let assert syntax.Gleam = syntax.language_from_path("src/main.gleam") 1126 let assert syntax.Toml = syntax.language_from_path("gleam.toml") 1127 let assert syntax.Markdown = syntax.language_from_path("README.md") 1128 let assert syntax.Plain = syntax.language_from_path("notes.txt") 1129 let assert syntax.Plain = syntax.language_from_path("Makefile") 1130 Nil 1131} 1132 1133pub fn syntax_detect_sniff_json_test() { 1134 let assert syntax.Json = syntax.detect("data", "{\"a\": 1}") 1135 let assert syntax.Markdown = syntax.detect("notes", "# Title\n\nbody") 1136 let assert syntax.Plain = syntax.detect("x", "just words") 1137 // Extension wins over sniff 1138 let assert syntax.Toml = syntax.detect("x.toml", "{\"a\": 1}") 1139 Nil 1140} 1141 1142pub fn syntax_is_binary_test() { 1143 let assert False = syntax.is_binary("hello\nworld\t!") 1144 let assert True = syntax.is_binary("a\u{0000}b") 1145 Nil 1146} 1147 1148pub fn syntax_paint_json_test() { 1149 let src = "{\"n\": 1, \"msg\": \"hi\", \"ok\": true}" 1150 let assert True = syntax.paint(False, syntax.Json, src) == src 1151 let painted = syntax.paint(True, syntax.Json, src) 1152 // Truecolor roles: keys (sky), string values (green), numbers, bools 1153 let assert True = string_contains(painted, "38;2") 1154 let assert True = string_contains(painted, "true") 1155 let assert True = string_contains(painted, "137;220;235") 1156 let assert True = string_contains(painted, "166;227;161") 1157 let assert True = string_contains(painted, "250;179;135") 1158 let assert True = color.strip_ansi(painted) == src 1159 Nil 1160} 1161 1162pub fn syntax_paint_gleam_test() { 1163 let src = "pub fn main() {\n // hi\n 42\n}" 1164 let painted = syntax.paint(True, syntax.Gleam, src) 1165 // keyword mauve, fn name blue, comment italic, number peach 1166 let assert True = string_contains(painted, "203;166;247") 1167 let assert True = string_contains(painted, "137;180;250") 1168 let assert True = string_contains(painted, "108;112;134") 1169 let assert True = string_contains(painted, "250;179;135") 1170 let assert True = string_contains(painted, "pub") 1171 let assert True = string_contains(painted, "main") 1172 let assert True = color.strip_ansi(painted) == src 1173 Nil 1174} 1175 1176pub fn syntax_paint_toml_test() { 1177 let src = "name = \"gleshell\"\n# comment\nenabled = true" 1178 let painted = syntax.paint(True, syntax.Toml, src) 1179 let assert True = string_contains(painted, "38;2") 1180 let assert True = string_contains(painted, "166;227;161") 1181 let assert True = string_contains(painted, "108;112;134") 1182 let assert True = color.strip_ansi(painted) == src 1183 Nil 1184} 1185 1186pub fn syntax_paint_markdown_test() { 1187 let src = "# Title\n\nUse `code` and **bold**.\n" 1188 let painted = syntax.paint(True, syntax.Markdown, src) 1189 let assert True = string_contains(painted, "Title") 1190 let assert True = string_contains(painted, "code") 1191 // H1 lavender + bold peach for **bold** 1192 let assert True = string_contains(painted, "180;190;254") 1193 let assert True = string_contains(painted, "250;179;135") 1194 let assert True = color.visible_length(painted) 1195 >= color.visible_length(src) - 1 1196 Nil 1197} 1198 1199pub fn syntax_frame_gutter_test() { 1200 let body = "alpha\nbeta" 1201 let framed = syntax.frame("src/demo.gleam", syntax.Gleam, body) 1202 let stripped = color.strip_ansi(framed) 1203 // Header carries basename + language badge 1204 let assert True = string_contains(stripped, "demo.gleam") 1205 let assert True = string_contains(stripped, "gleam") 1206 // Line numbers + pipe gutter 1207 let assert True = string_contains(stripped, "1") 1208 let assert True = string_contains(stripped, "2") 1209 let assert True = string_contains(stripped, "") 1210 let assert True = string_contains(stripped, "alpha") 1211 let assert True = string_contains(stripped, "beta") 1212 Nil 1213} 1214 1215pub fn cat_raw_and_language_test() { 1216 let env = env.new() 1217 // Write a temp json file under the project (simplifile needs a real path). 1218 let path = "build/cat_syntax_test.json" 1219 let body = "{\"x\": 1}" 1220 let assert Ok(Nil) = simplifile.write(to: path, contents: body) 1221 1222 // --raw must return plain content even when colors would be on. 1223 let assert eval.Continue(_, String(raw_out)) = 1224 eval.eval_source(env, "cat " <> path <> " --raw") 1225 let assert True = raw_out == body 1226 1227 // --language plain: no syntax colors (may still frame with line numbers on TTY). 1228 let assert eval.Continue(_, String(plain_out)) = 1229 eval.eval_source(env, "cat " <> path <> " --language plain") 1230 let plain_stripped = color.strip_ansi(plain_out) 1231 let assert True = 1232 plain_out == body 1233 || plain_stripped == body 1234 || string_contains(plain_stripped, body) 1235 1236 // Forced gleam: visible body still present (gutter/header OK). 1237 let assert eval.Continue(_, String(gleam_out)) = 1238 eval.eval_source(env, "cat " <> path <> " --language gleam") 1239 let gleam_stripped = color.strip_ansi(gleam_out) 1240 let assert True = 1241 gleam_stripped == body || string_contains(gleam_stripped, body) 1242 1243 // Unknown language errors. 1244 let assert eval.Continue(_, value.Fail(msg)) = 1245 eval.eval_source(env, "cat " <> path <> " --language cobol") 1246 let assert True = string.contains(msg, "unknown language") 1247 1248 let _ = simplifile.delete(path) 1249 Nil 1250} 1251 1252// --- tab completion --- 1253 1254pub fn complete_command_builtin_test() { 1255 // Start of line: complete builtins 1256 let #(matches, kind) = sys.complete_word("", "ech") 1257 let assert True = kind == "command" 1258 let assert True = list_contains(matches, "echo") 1259 // After pipeline separator 1260 let #(matches2, kind2) = sys.complete_word("ls | ", "wher") 1261 let assert True = kind2 == "command" 1262 let assert True = list_contains(matches2, "where") 1263 // `to` / `from` are single-word commands (subcommand is a normal arg) 1264 let #(matches3, kind3) = sys.complete_word("", "to") 1265 let assert True = kind3 == "command" 1266 let assert True = list_contains(matches3, "to") 1267 let #(matches_from, kind_from) = sys.complete_word("", "fro") 1268 let assert True = kind_from == "command" 1269 let assert True = list_contains(matches_from, "from") 1270 // Keyword 1271 let #(matches4, kind4) = sys.complete_word("", "le") 1272 let assert True = kind4 == "command" 1273 let assert True = list_contains(matches4, "let") 1274 Nil 1275} 1276 1277pub fn complete_command_after_assign_test() { 1278 let #(matches, kind) = sys.complete_word("let x = ", "ran") 1279 let assert True = kind == "command" 1280 let assert True = list_contains(matches, "range") 1281 Nil 1282} 1283 1284pub fn complete_path_for_args_test() { 1285 // After a command name, Tab completes files not commands 1286 let #(_matches, kind) = sys.complete_word("echo ", "ech") 1287 let assert True = kind == "path" 1288 Nil 1289} 1290 1291pub fn complete_path_like_command_test() { 1292 // Path-shaped command words stay on filename completion 1293 let #(_matches, kind) = sys.complete_word("", "./ec") 1294 let assert True = kind == "path" 1295 let #(_matches2, kind2) = sys.complete_word("", "/us") 1296 let assert True = kind2 == "path" 1297 Nil 1298} 1299 1300pub fn complete_path_executable_test() { 1301 // PATH + builtins for a non-empty prefix 1302 let #(matches, kind) = sys.complete_word("", "s") 1303 let assert True = kind == "command" 1304 // At least builtins starting with s (select, skip, sort-by, …) 1305 let assert True = list_contains(matches, "select") 1306 Nil 1307} 1308 1309// --- history ghost-text hints (Nu/fish style) --- 1310 1311pub fn history_hint_newest_prefix_test() { 1312 // Newest-first: first entry that has the buffer as a proper prefix wins. 1313 let hist = ["ls | where type == file", "ls | first 3", "echo hi"] 1314 let assert " | where type == file" = sys.history_hint(hist, "ls") 1315 let assert " | first 3" = sys.history_hint(["ls | first 3", "ls | where x"], "ls") 1316 let assert " type == file" = sys.history_hint(hist, "ls | where") 1317 let assert " hi" = sys.history_hint(hist, "echo") 1318 Nil 1319} 1320 1321pub fn history_hint_no_match_test() { 1322 let hist = ["ls", "echo hi"] 1323 let assert "" = sys.history_hint(hist, "cd") 1324 // Exact match only — no suffix 1325 let assert "" = sys.history_hint(hist, "ls") 1326 // Empty buffer never suggests 1327 let assert "" = sys.history_hint(hist, "") 1328 Nil 1329} 1330 1331// --- Ctrl+R reverse search filter (stinkpot-style fuzzy) --- 1332 1333pub fn history_search_empty_query_returns_all_test() { 1334 let hist = ["ls | first 3", "echo hi", "cd src"] 1335 let assert ["ls | first 3", "echo hi", "cd src"] = sys.history_search(hist, "") 1336 let assert ["ls | first 3", "echo hi", "cd src"] = 1337 sys.history_search(hist, " ") 1338 Nil 1339} 1340 1341pub fn history_search_dedupes_newest_first_test() { 1342 let hist = ["echo a", "ls", "echo a", "cd"] 1343 let assert ["echo a", "ls", "cd"] = sys.history_search(hist, "") 1344 Nil 1345} 1346 1347pub fn history_search_substring_and_fuzzy_test() { 1348 let hist = [ 1349 "gleam test", 1350 "ls | where type == file", 1351 "git status", 1352 "echo hello", 1353 ] 1354 let hits = sys.history_search(hist, "gleam") 1355 let assert True = list_contains(hits, "gleam test") 1356 let assert False = list_contains(hits, "git status") 1357 1358 // Subsequence: "gts" matches "git status" 1359 let fuzzy = sys.history_search(hist, "gts") 1360 let assert True = list_contains(fuzzy, "git status") 1361 1362 // Case-insensitive 1363 let assert True = 1364 list_contains(sys.history_search(hist, "GLEAM"), "gleam test") 1365 Nil 1366} 1367 1368pub fn history_search_no_match_test() { 1369 let hist = ["ls", "echo hi"] 1370 let assert [] = sys.history_search(hist, "zzzz-nope") 1371 Nil 1372} 1373 1374fn list_contains(items: List(String), needle: String) -> Bool { 1375 case items { 1376 [] -> False 1377 [x, ..] if x == needle -> True 1378 [_, ..rest] -> list_contains(rest, needle) 1379 } 1380}