From 400d69d623b82b1866cc66343cd1846fa21a33f8 Mon Sep 17 00:00:00 2001 From: barkure <43804451+barkure@users.noreply.github.com> Date: Wed, 1 Apr 2026 23:31:47 +0800 Subject: [PATCH 1/3] lsh: add Bash, Zsh, Python and JavaScript highlighting --- assets/highlighting-tests/javascript.js | 59 ++++++++++++++++++++ assets/highlighting-tests/markdown.md | 11 ++++ assets/highlighting-tests/python.py | 48 ++++++++++++++++ assets/highlighting-tests/zsh.sh | 40 +++++++++++++ crates/lsh/definitions/bash.lsh | 61 ++++++++++++++++++++ crates/lsh/definitions/javascript.lsh | 74 +++++++++++++++++++++++++ crates/lsh/definitions/markdown.lsh | 42 +++++++++++++- crates/lsh/definitions/python.lsh | 71 ++++++++++++++++++++++++ crates/lsh/definitions/zsh.lsh | 60 ++++++++++++++++++++ 9 files changed, 465 insertions(+), 1 deletion(-) create mode 100644 assets/highlighting-tests/javascript.js create mode 100644 assets/highlighting-tests/python.py create mode 100644 assets/highlighting-tests/zsh.sh create mode 100644 crates/lsh/definitions/bash.lsh create mode 100644 crates/lsh/definitions/javascript.lsh create mode 100644 crates/lsh/definitions/python.lsh create mode 100644 crates/lsh/definitions/zsh.lsh diff --git a/assets/highlighting-tests/javascript.js b/assets/highlighting-tests/javascript.js new file mode 100644 index 00000000000..53c000b1f2b --- /dev/null +++ b/assets/highlighting-tests/javascript.js @@ -0,0 +1,59 @@ +// Single-line comment +/* Multi-line + comment */ + +const single = 'single quoted string'; +const double = "double quoted string"; +const template = `template string with ${single}`; + +const decimal = 42; +const negative = -3.14e+2; +const hex = 0x2a; +const binary = 0b101010; +const octal = 0o52; + +const truthy = true; +const falsy = false; +const empty = null; +const missing = undefined; +const notANumber = NaN; +const infinite = Infinity; + +export async function greet(name) { + if (name instanceof String) { + return; + } else if (name in { user: "ok" }) { + throw new Error("unexpected"); + } + + for (let i = 0; i < 3; i++) { + while (false) { + break; + } + } + + try { + return console.log(template, name); + } catch (error) { + return void error; + } finally { + delete globalThis.temp; + } +} + +class Person extends Object { + constructor(name) { + super(); + this.name = name; + } +} + +const result = greet("world"); +switch (result) { + case true: + break; + default: + continueLabel: do { + break continueLabel; + } while (false); +} diff --git a/assets/highlighting-tests/markdown.md b/assets/highlighting-tests/markdown.md index c4845eb6801..555b182f961 100644 --- a/assets/highlighting-tests/markdown.md +++ b/assets/highlighting-tests/markdown.md @@ -66,6 +66,12 @@ Reference: ![Logo][logo-ref] echo "Hello, world" | tr a-z A-Z ``` +```javascript +export function greet(name) { + return `hello ${name}`; +} +``` + ```json { "name": "gfm-kitchen-sink", @@ -73,3 +79,8 @@ echo "Hello, world" | tr a-z A-Z "scripts": { "test": "echo ok" } } ``` + +```python +def greet(name: str) -> str: + return f"hello {name}" +``` diff --git a/assets/highlighting-tests/python.py b/assets/highlighting-tests/python.py new file mode 100644 index 00000000000..4c49bbd9a59 --- /dev/null +++ b/assets/highlighting-tests/python.py @@ -0,0 +1,48 @@ +# Single-line comment +'''Triple single quoted string''' +"""Triple double quoted string""" + +single = 'single quoted string' +double = "double quoted string" + +decimal = 42 +negative = -3.14e+2 +hex_value = 0x2A +binary_value = 0b101010 +octal_value = 0o52 +complex_value = 1.5j + +truthy = True +falsy = False +nothing = None + +@decorator +async def greet(name: str) -> None: + value = f"Hello, {name}" + + if value and name is not None: + print(value) + elif value in {"hello", "world"}: + raise ValueError("unexpected") + else: + return None + + for item in [single, double]: + while False: + break + + try: + assert item + except Exception as exc: + yield exc + finally: + pass + + +class Person: + def __init__(self, name): + self.name = name + + +result = greet("world") +lambda_value = lambda x: x + 1 diff --git a/assets/highlighting-tests/zsh.sh b/assets/highlighting-tests/zsh.sh new file mode 100644 index 00000000000..46ae4cf461a --- /dev/null +++ b/assets/highlighting-tests/zsh.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env zsh +# zsh-style shell sample + +setopt autocd extendedglob + +typeset -g POWERLEVEL="lean" +readonly ZSH_THEME="agnoster" + +plugins=(git docker fzf) + +alias ll='ls -lah' +alias gs='git status' + +path=("$HOME/bin" $path) +export EDITOR="nvim" + +function mkcd() { + local dir="$1" + mkdir -p "$dir" && cd "$dir" +} + +if [[ -n "$HOME" ]]; then + echo "home is $HOME" +elif [[ -z "$HOME" ]]; then + echo "missing home" +else + echo "unexpected" +fi + +for plugin in $plugins; do + echo "$plugin" +done + +case "$ZSH_THEME" in + agnoster) echo "theme selected" ;; + *) echo "default theme" ;; +esac + +source "$HOME/.zsh_aliases" +mkcd "${HOME}/tmp" diff --git a/crates/lsh/definitions/bash.lsh b/crates/lsh/definitions/bash.lsh new file mode 100644 index 00000000000..bdb87c7c240 --- /dev/null +++ b/crates/lsh/definitions/bash.lsh @@ -0,0 +1,61 @@ +#[display_name = "Bash"] +#[path = "**/*.bash"] +#[path = "**/*.sh"] +#[path = "**/.bash_profile"] +#[path = "**/.bashrc"] +#[path = "**/.profile"] +pub fn bash() { + until /$/ { + yield other; + + if /#.*/ { + yield comment; + } else if /'/ { + until /$/ { + yield string; + if /'/ { yield string; break; } + await input; + } + } else if /"/ { + until /$/ { + yield string; + if /\\./ {} + else if /"/ { yield string; break; } + await input; + } + } else if /`/ { + until /$/ { + yield string; + if /\\./ {} + else if /`/ { yield string; break; } + await input; + } + } else if /\$\{[^}]+\}|\$[A-Za-z_]\w*|\$\d+|\$[#?*!@$-]/ { + yield variable; + } else if /(?:if|then|elif|else|fi|for|while|until|do|done|case|esac|in|select)\>/ { + if /\w+/ { + yield other; + } else { + yield keyword.control; + } + } else if /(?:declare|export|function|local|readonly|source)\>/ { + if /\w+/ { + yield other; + } else { + yield keyword.other; + } + } else if /-?(?:0[xX][\da-fA-F]+|\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/ { + if /\w+/ { + yield other; + } else { + yield constant.numeric; + } + } else if /([A-Za-z_][\w-]*)\s*\(/ { + yield $1 as method; + } else if /[A-Za-z_][\w-]*/ { + // Gobble any other tokens that should not be highlighted + } + + yield other; + } +} diff --git a/crates/lsh/definitions/javascript.lsh b/crates/lsh/definitions/javascript.lsh new file mode 100644 index 00000000000..7c4a4656b05 --- /dev/null +++ b/crates/lsh/definitions/javascript.lsh @@ -0,0 +1,74 @@ +#[display_name = "JavaScript"] +#[path = "**/*.cjs"] +#[path = "**/*.js"] +#[path = "**/*.jsx"] +#[path = "**/*.mjs"] +pub fn javascript() { + until /$/ { + yield other; + + if /\/\/.*/ { + yield comment; + } else if /\/\*/ { + loop { + yield comment; + await input; + if /\*\// { + yield comment; + break; + } + } + } else if /'/ { + until /$/ { + yield string; + if /\\./ {} + else if /'/ { yield string; break; } + await input; + } + } else if /"/ { + until /$/ { + yield string; + if /\\./ {} + else if /"/ { yield string; break; } + await input; + } + } else if /`/ { + loop { + yield string; + if /\\./ {} + else if /`/ { yield string; break; } + await input; + } + } else if /(?:if|else|switch|case|default|for|while|do|break|continue|try|catch|finally|throw|return)\>/ { + if /\w+/ { + yield other; + } else { + yield keyword.control; + } + } else if /(?:async|await|class|const|delete|export|extends|function|import|in|instanceof|let|new|of|super|this|typeof|var|void|yield)\>/ { + if /\w+/ { + yield other; + } else { + yield keyword.other; + } + } else if /(?:true|false|null|undefined|NaN|Infinity)\>/ { + if /\w+/ { + yield other; + } else { + yield constant.language; + } + } else if /-?(?:0[xX][\da-fA-F]+|0[bB][01]+|0[oO][0-7]+|\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/ { + if /\w+/ { + yield other; + } else { + yield constant.numeric; + } + } else if /([A-Za-z_$][\w$]*)\s*\(/ { + yield $1 as method; + } else if /[A-Za-z_$][\w$]*/ { + // Gobble any other tokens that should not be highlighted + } + + yield other; + } +} diff --git a/crates/lsh/definitions/markdown.lsh b/crates/lsh/definitions/markdown.lsh index 3fcc7721a15..fdb43a2e770 100644 --- a/crates/lsh/definitions/markdown.lsh +++ b/crates/lsh/definitions/markdown.lsh @@ -18,7 +18,27 @@ pub fn markdown() { yield comment; } else if /```/ { // NOTE: These checks are sorted alphabetically. - if /(?i:diff)/ { + if /(?i:bash|sh|shell)/ { + loop { + await input; + if /\s*```/ { + return; + } else { + bash(); + if /.*/ {} + } + } + } else if /(?i:zsh)/ { + loop { + await input; + if /\s*```/ { + return; + } else { + zsh(); + if /.*/ {} + } + } + } else if /(?i:diff)/ { loop { await input; if /\s*```/ { @@ -30,6 +50,16 @@ pub fn markdown() { if /.*/ {} } } + } else if /(?i:javascript|js|jsx|mjs|cjs)/ { + loop { + await input; + if /\s*```/ { + return; + } else { + javascript(); + if /.*/ {} + } + } } else if /(?i:json)/ { loop { await input; @@ -40,6 +70,16 @@ pub fn markdown() { if /.*/ {} } } + } else if /(?i:py|python)/ { + loop { + await input; + if /\s*```/ { + return; + } else { + python(); + if /.*/ {} + } + } } else if /(?i:yaml)/ { loop { await input; diff --git a/crates/lsh/definitions/python.lsh b/crates/lsh/definitions/python.lsh new file mode 100644 index 00000000000..7303b8968d4 --- /dev/null +++ b/crates/lsh/definitions/python.lsh @@ -0,0 +1,71 @@ +#[display_name = "Python"] +#[path = "**/*.py"] +#[path = "**/*.pyi"] +#[path = "**/*.pyw"] +pub fn python() { + until /$/ { + yield other; + + if /#.*/ { + yield comment; + } else if /'''/ { + loop { + yield string; + if /'''/ { yield string; break; } + await input; + } + } else if /"""/ { + loop { + yield string; + if /"""/ { yield string; break; } + await input; + } + } else if /'/ { + until /$/ { + yield string; + if /\\./ {} + else if /'/ { yield string; break; } + await input; + } + } else if /"/ { + until /$/ { + yield string; + if /\\./ {} + else if /"/ { yield string; break; } + await input; + } + } else if /(?:if|elif|else|for|while|try|except|finally|return|break|continue|raise|with|match|case|pass)\>/ { + if /\w+/ { + yield other; + } else { + yield keyword.control; + } + } else if /(?:and|as|assert|async|await|class|def|del|from|global|import|in|is|lambda|nonlocal|not|or|yield)\>/ { + if /\w+/ { + yield other; + } else { + yield keyword.other; + } + } else if /(?:True|False|None)\>/ { + if /\w+/ { + yield other; + } else { + yield constant.language; + } + } else if /-?(?:0[xX][\da-fA-F]+|0[bB][01]+|0[oO][0-7]+|\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?(?:[jJ])?/ { + if /\w+/ { + yield other; + } else { + yield constant.numeric; + } + } else if /@[A-Za-z_]\w*/ { + yield markup.link; + } else if /([A-Za-z_]\w*)\s*\(/ { + yield $1 as method; + } else if /[A-Za-z_]\w*/ { + // Gobble any other tokens that should not be highlighted + } + + yield other; + } +} diff --git a/crates/lsh/definitions/zsh.lsh b/crates/lsh/definitions/zsh.lsh new file mode 100644 index 00000000000..b8c5a21e4fe --- /dev/null +++ b/crates/lsh/definitions/zsh.lsh @@ -0,0 +1,60 @@ +#[display_name = "Zsh"] +#[path = "**/*.zsh"] +#[path = "**/.zprofile"] +#[path = "**/.zshenv"] +#[path = "**/.zshrc"] +pub fn zsh() { + until /$/ { + yield other; + + if /#.*/ { + yield comment; + } else if /'/ { + until /$/ { + yield string; + if /'/ { yield string; break; } + await input; + } + } else if /"/ { + until /$/ { + yield string; + if /\\./ {} + else if /"/ { yield string; break; } + await input; + } + } else if /`/ { + until /$/ { + yield string; + if /\\./ {} + else if /`/ { yield string; break; } + await input; + } + } else if /\$\{[^}]+\}|\$[A-Za-z_]\w*|\$\d+|\$[#?*!@$-]/ { + yield variable; + } else if /(?:if|then|elif|else|fi|for|while|until|do|done|case|esac|in|select)\>/ { + if /\w+/ { + yield other; + } else { + yield keyword.control; + } + } else if /(?:alias|autoload|bindkey|compdef|declare|emulate|export|function|local|readonly|setopt|source|typeset|unalias|unsetopt|zmodload)\>/ { + if /\w+/ { + yield other; + } else { + yield keyword.other; + } + } else if /-?(?:0[xX][\da-fA-F]+|\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/ { + if /\w+/ { + yield other; + } else { + yield constant.numeric; + } + } else if /([A-Za-z_][\w-]*)\s*\(/ { + yield $1 as method; + } else if /[A-Za-z_][\w-]*/ { + // Gobble any other tokens that should not be highlighted + } + + yield other; + } +} From 34aec22826c21a73ebfdeecbde20c4cf199ccd26 Mon Sep 17 00:00:00 2001 From: barkure <43804451+barkure@users.noreply.github.com> Date: Tue, 7 Apr 2026 12:42:45 +0800 Subject: [PATCH 2/3] lsh: tighten numeric regex patterns --- crates/lsh/definitions/bash.lsh | 2 +- crates/lsh/definitions/javascript.lsh | 2 +- crates/lsh/definitions/python.lsh | 2 +- crates/lsh/definitions/zsh.lsh | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/lsh/definitions/bash.lsh b/crates/lsh/definitions/bash.lsh index bdb87c7c240..433cb0f2ffd 100644 --- a/crates/lsh/definitions/bash.lsh +++ b/crates/lsh/definitions/bash.lsh @@ -44,7 +44,7 @@ pub fn bash() { } else { yield keyword.other; } - } else if /-?(?:0[xX][\da-fA-F]+|\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/ { + } else if /-?(?:(?i:0x[\da-fA-F]+)|\d+\.?\d*|\.\d+)(?i:e[+-]?\d+)?/ { if /\w+/ { yield other; } else { diff --git a/crates/lsh/definitions/javascript.lsh b/crates/lsh/definitions/javascript.lsh index 7c4a4656b05..8c496a2278e 100644 --- a/crates/lsh/definitions/javascript.lsh +++ b/crates/lsh/definitions/javascript.lsh @@ -57,7 +57,7 @@ pub fn javascript() { } else { yield constant.language; } - } else if /-?(?:0[xX][\da-fA-F]+|0[bB][01]+|0[oO][0-7]+|\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/ { + } else if /-?(?:(?i:0x[\da-fA-F]+)|(?i:0b[01]+)|(?i:0o[0-7]+)|\d+\.?\d*|\.\d+)(?i:e[+-]?\d+)?/ { if /\w+/ { yield other; } else { diff --git a/crates/lsh/definitions/python.lsh b/crates/lsh/definitions/python.lsh index 7303b8968d4..becaed7fb89 100644 --- a/crates/lsh/definitions/python.lsh +++ b/crates/lsh/definitions/python.lsh @@ -52,7 +52,7 @@ pub fn python() { } else { yield constant.language; } - } else if /-?(?:0[xX][\da-fA-F]+|0[bB][01]+|0[oO][0-7]+|\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?(?:[jJ])?/ { + } else if /-?(?:(?i:0x[\da-fA-F]+)|(?i:0b[01]+)|(?i:0o[0-7]+)|\d+\.?\d*|\.\d+)(?i:e[+-]?\d+)?(?i:j)?/ { if /\w+/ { yield other; } else { diff --git a/crates/lsh/definitions/zsh.lsh b/crates/lsh/definitions/zsh.lsh index b8c5a21e4fe..f22a4fff245 100644 --- a/crates/lsh/definitions/zsh.lsh +++ b/crates/lsh/definitions/zsh.lsh @@ -43,7 +43,7 @@ pub fn zsh() { } else { yield keyword.other; } - } else if /-?(?:0[xX][\da-fA-F]+|\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/ { + } else if /-?(?:(?i:0x[\da-fA-F]+)|\d+\.?\d*|\.\d+)(?i:e[+-]?\d+)?/ { if /\w+/ { yield other; } else { From ed64e6460fcf45110717c69d3bc6e6a8834efa7b Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 7 Apr 2026 21:45:53 +0200 Subject: [PATCH 3/3] Unified Bash/ZSH into shellscript.lsh, Further improvements --- assets/highlighting-tests/bash.sh | 71 ------- assets/highlighting-tests/javascript.js | 154 ++++++++++----- assets/highlighting-tests/python.py | 174 +++++++++++++---- assets/highlighting-tests/shellscript.sh | 228 +++++++++++++++++++++++ assets/highlighting-tests/zsh.sh | 40 ---- crates/lsh/definitions/bash.lsh | 61 ------ crates/lsh/definitions/javascript.lsh | 32 +--- crates/lsh/definitions/markdown.lsh | 18 +- crates/lsh/definitions/python.lsh | 34 ++-- crates/lsh/definitions/shellscript.lsh | 87 +++++++++ crates/lsh/definitions/zsh.lsh | 60 ------ 11 files changed, 580 insertions(+), 379 deletions(-) delete mode 100644 assets/highlighting-tests/bash.sh create mode 100644 assets/highlighting-tests/shellscript.sh delete mode 100644 assets/highlighting-tests/zsh.sh delete mode 100644 crates/lsh/definitions/bash.lsh create mode 100644 crates/lsh/definitions/shellscript.lsh delete mode 100644 crates/lsh/definitions/zsh.lsh diff --git a/assets/highlighting-tests/bash.sh b/assets/highlighting-tests/bash.sh deleted file mode 100644 index dfd5238724d..00000000000 --- a/assets/highlighting-tests/bash.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash - -# This is a comment - -readonly VAR1="Hello" # String literal -VAR2=42 # Integer literal -VAR3=$((VAR2 + 8)) # Arithmetic expansion -VAR4=$(echo "World") # Command substitution - -function greet() { # Function definition - local name="$1" # Local variable, parameter expansion - echo "${VAR1}, $name! $VAR4" # String, parameter expansion, variable -} - -greet "User" # Function call, string literal - -if [[ $VAR2 -gt 40 && $VAR3 -eq 50 ]]; then # Conditional, test, operators - echo "Numbers are correct" # String literal -elif (( VAR2 < 40 )); then # Arithmetic test - echo 'VAR2 is less than 40' # Single-quoted string -else - echo "Other case" -fi - -for i in {1..3}; do # Brace expansion, for loop - echo "Loop $i" # String, variable -done - -case "$VAR4" in # Case statement - World) echo "It's World";; # Pattern, string - *) echo "Unknown";; # Wildcard -esac - -arr=(one two three) # Array -echo "${arr[1]}" # Array access - -declare -A assoc # Associative array -assoc[key]="value" -echo "${assoc[key]}" - -# Here document -cat < /dev/null - -# Background job -sleep 1 & - -# Arithmetic assignment -let VAR2+=1 - -# Process substitution -diff <(echo foo) <(echo bar) - -# Command grouping -{ echo "Group 1"; echo "Group 2"; } - -# Escaped characters -echo "A quote: \" and a backslash: \\" - -# End of file diff --git a/assets/highlighting-tests/javascript.js b/assets/highlighting-tests/javascript.js index 53c000b1f2b..81323e48d7d 100644 --- a/assets/highlighting-tests/javascript.js +++ b/assets/highlighting-tests/javascript.js @@ -1,59 +1,117 @@ +// Comments // Single-line comment -/* Multi-line - comment */ - -const single = 'single quoted string'; -const double = "double quoted string"; -const template = `template string with ${single}`; - -const decimal = 42; -const negative = -3.14e+2; -const hex = 0x2a; -const binary = 0b101010; -const octal = 0o52; - -const truthy = true; -const falsy = false; -const empty = null; -const missing = undefined; -const notANumber = NaN; -const infinite = Infinity; - -export async function greet(name) { - if (name instanceof String) { - return; - } else if (name in { user: "ok" }) { - throw new Error("unexpected"); - } - for (let i = 0; i < 3; i++) { - while (false) { - break; - } - } +/* + * Multi-line + * comment + */ - try { - return console.log(template, name); - } catch (error) { - return void error; - } finally { - delete globalThis.temp; - } +// Numbers +42; +3.14; +.5; +1e10; +1.5e-3; +0xff; +0xFF; +0b1010; +0o77; +1_000_000; +42n; + +// Constants +true; +false; +null; +undefined; +NaN; +Infinity; + +// Strings +'single quotes with escape: \' \n \t \\'; +"double quotes with escape: \" \n \t \\"; + +// Control flow keywords +if (true) { +} else if (false) { +} else { +} + +for (let i = 0; i < 10; i++) { + if (i === 5) continue; + if (i === 8) break; +} + +while (false) { } +do { } while (false); + +switch (42) { + case 1: break; + default: break; +} + +try { + throw new Error("oops"); +} catch (e) { +} finally { +} + +debugger; + +// Template literals +`template literal: ${1 + 2} and ${greet("world")}`; +`multi +line +template`; + +// Other keywords (some are contextually reserved) +var a = 1; +let b = 2; +const c = 3; + +function greet(name) { + return "Hello, " + name; +} + +async function fetchData() { + const result = await fetch("/api"); + return result; +} + +function* gen() { + yield 1; + yield 2; } -class Person extends Object { +class Animal extends Object { + static count = 0; + constructor(name) { super(); this.name = name; + Animal.count++; } -} -const result = greet("world"); -switch (result) { - case true: - break; - default: - continueLabel: do { - break continueLabel; - } while (false); + speak() { + return `${this.name} speaks`; + } } + +const obj = { a: 1 }; +delete obj.a; +typeof obj; +void 0; +"a" instanceof Object; +"a" in obj; + +import { readFile } from "fs"; +export const PI = 3.14; +for (const x of [1, 2]) { } +for (const k in { a: 1 }) { } + +// Function calls +console.log("hello"); +Math.max(1, 2); +[1, 2, 3].map(x => x * 2); +greet("world"); +parseInt("42"); diff --git a/assets/highlighting-tests/python.py b/assets/highlighting-tests/python.py index 4c49bbd9a59..03dcf0d90be 100644 --- a/assets/highlighting-tests/python.py +++ b/assets/highlighting-tests/python.py @@ -1,48 +1,142 @@ +# Comments # Single-line comment -'''Triple single quoted string''' -"""Triple double quoted string""" - -single = 'single quoted string' -double = "double quoted string" - -decimal = 42 -negative = -3.14e+2 -hex_value = 0x2A -binary_value = 0b101010 -octal_value = 0o52 -complex_value = 1.5j - -truthy = True -falsy = False -nothing = None - -@decorator -async def greet(name: str) -> None: - value = f"Hello, {name}" - - if value and name is not None: - print(value) - elif value in {"hello", "world"}: - raise ValueError("unexpected") - else: - return None - - for item in [single, double]: - while False: - break - - try: - assert item - except Exception as exc: - yield exc - finally: + +# Numbers +42 +3.14 +.5 +1e10 +1.5e-3 +0xff +0xFF +0b1010 +0o77 +1_000_000 +3.14j + +# Constants +True +False +None + +# Strings +'single quotes: \' \n \t \\' +"double quotes: \" \n \t \\" + +# Control flow keywords +if True: + pass +elif False: + pass +else: + pass + +for i in range(10): + if i == 5: + continue + if i == 8: + break + +while False: + pass + +match 42: + case 1: pass + case _: + pass + +try: + raise ValueError("oops") +except ValueError as e: + pass +finally: + pass + +with open("/dev/null") as f: + pass + +return # (only valid inside a function) + +# Triple-quoted strings +""" +Multi-line +docstring (double quotes) +""" + +''' +Multi-line +string (single quotes) +''' +# Prefixed strings (f, r, b) +f"f-string: {1 + 2}" +r"raw string: \n is literal" +b"byte string" + +# Decorators +@staticmethod +def helper(): + pass + +@property +def name(self): + return self._name + +@custom_decorator +def decorated(): + pass + +# Other keywords (some are contextually reserved) +import os +from os import path +import sys as system + +def greet(name): + return "Hello, " + name + +async def fetch_data(): + result = await some_coroutine() + return result + +class Animal: + count = 0 -class Person: def __init__(self, name): self.name = name + Animal.count += 1 + + def speak(self): + return f"{self.name} speaks" + +class Dog(Animal): + pass + +lambda x: x + 1 + +x = 1 +del x +assert True +not False +True and False +True or False +1 is 1 +1 is not 2 +1 in [1, 2] + +global _g +nonlocal # (only valid inside nested function) + +def gen(): + yield 1 + yield from [2, 3] +type Alias = int -result = greet("world") -lambda_value = lambda x: x + 1 +# Function calls +print("hello") +len([1, 2, 3]) +list(range(10)) +greet("world") +int("42") +"hello".upper() diff --git a/assets/highlighting-tests/shellscript.sh b/assets/highlighting-tests/shellscript.sh new file mode 100644 index 00000000000..d263b88d0fd --- /dev/null +++ b/assets/highlighting-tests/shellscript.sh @@ -0,0 +1,228 @@ +#!/bin/bash + +# Numbers +42 +3.14 +-7 +0xFF +0777 +2#1010 + +# Constants +true +false + +# Single-quoted strings +echo 'hello world' +echo 'it'\''s a trap' + +# Double-quoted strings +echo "hello $USER" +echo "home is ${HOME}" +echo "escaped \$ and \" and \\" +echo "subshell: $(whoami)" +echo "arithmetic: $((1 + 2))" +echo "positional: $1 $# $? $! $@ $0 $$ $-" + +# ANSI-C quoting +echo $'tab:\there\nnewline' +echo $'escape sequences: \a \b \e \f \r \t \v \\\\ \'' + +# Backtick interpolation +echo "today is `date +%Y-%m-%d`" + +# Control flow +if [ -f /etc/passwd ]; then + echo "exists" +elif [ -d /tmp ]; then + echo "tmp exists" +else + echo "neither" +fi + +for i in 1 2 3; do + echo "$i" + continue +done + +for ((i = 0; i < 3; i++)); do + break +done + +while true; do + break +done + +until false; do + break +done + +case "$1" in + start) echo "starting" ;; + stop) echo "stopping" ;; + *) echo "unknown" ;; +esac + +select opt in "yes" "no" "quit"; do + echo "$opt" + break +done + +time ls -la + +# Test expressions +[ -f /etc/passwd ] +[ -d /tmp ] +[ -z "$var" ] +[ -n "$var" ] +[ "$a" = "$b" ] +[ "$a" != "$b" ] +[ "$a" -eq 1 ] +[ "$a" -ne 2 ] +[ "$a" -lt 3 ] +[ "$a" -gt 4 ] +[ "$a" -le 5 ] +[ "$a" -ge 6 ] +[[ "$name" == *.txt ]] +[[ "$name" =~ ^[0-9]+$ ]] +[[ -f /etc/passwd && -d /tmp ]] +[[ -f /etc/passwd || -d /tmp ]] + +# Heredocs +cat <>= 1 )) +(( x &= 0xFF )) + +# Redirections +echo "out" > /dev/null +echo "append" >> /tmp/log +cat < /etc/passwd +echo "stderr" 2> /dev/null +echo "both" &> /dev/null +echo "dup" 2>&1 +exec 3<> /tmp/fd3 + +# Process substitution +diff <(ls /bin) <(ls /usr/bin) +tee >(grep error > errors.log) > /dev/null + +# Pipelines and logical operators +echo hello | cat +echo hello |& cat +ls && echo ok +ls || echo fail +sleep 10 & + +# Subshells and group commands +(cd /tmp && ls) +{ echo one; echo two; } + +# Extended globbing +shopt -s extglob +ls *.txt +ls ?(a|b) +ls *(a|b) +ls +(a|b) +ls @(a|b) +ls !(a|b) +echo ~ + +# Other keywords +export PATH="/usr/local/bin:$PATH" +local count=0 +readonly PI=3 +declare -a arr=(1 2 3) +declare -A map=([key]=value) +typeset -i num=42 +alias ll='ls -la' +source /dev/null +. /dev/null + +# Builtins +eval 'echo hello' +exec /bin/bash +trap 'echo bye' EXIT +test -f /etc/passwd +read -r line +printf "%s\n" "hello" +wait $! +kill -9 $$ +jobs -l +fg %1 +bg %1 +cd /tmp +pwd +set -euo pipefail +unset name +shift +getopts "ab:" opt +command ls +builtin echo +type ls +hash -r +ulimit -n +umask 022 +dirs +pushd /tmp +popd +disown %1 +let "x = 1 + 2" +exit 0 + +# Functions +function greet() { + local who="${1:-world}" + echo "Hello, $who" + return 0 +} + +cleanup() { + echo "done" +} + +greet "shell" +cleanup diff --git a/assets/highlighting-tests/zsh.sh b/assets/highlighting-tests/zsh.sh deleted file mode 100644 index 46ae4cf461a..00000000000 --- a/assets/highlighting-tests/zsh.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env zsh -# zsh-style shell sample - -setopt autocd extendedglob - -typeset -g POWERLEVEL="lean" -readonly ZSH_THEME="agnoster" - -plugins=(git docker fzf) - -alias ll='ls -lah' -alias gs='git status' - -path=("$HOME/bin" $path) -export EDITOR="nvim" - -function mkcd() { - local dir="$1" - mkdir -p "$dir" && cd "$dir" -} - -if [[ -n "$HOME" ]]; then - echo "home is $HOME" -elif [[ -z "$HOME" ]]; then - echo "missing home" -else - echo "unexpected" -fi - -for plugin in $plugins; do - echo "$plugin" -done - -case "$ZSH_THEME" in - agnoster) echo "theme selected" ;; - *) echo "default theme" ;; -esac - -source "$HOME/.zsh_aliases" -mkcd "${HOME}/tmp" diff --git a/crates/lsh/definitions/bash.lsh b/crates/lsh/definitions/bash.lsh deleted file mode 100644 index 433cb0f2ffd..00000000000 --- a/crates/lsh/definitions/bash.lsh +++ /dev/null @@ -1,61 +0,0 @@ -#[display_name = "Bash"] -#[path = "**/*.bash"] -#[path = "**/*.sh"] -#[path = "**/.bash_profile"] -#[path = "**/.bashrc"] -#[path = "**/.profile"] -pub fn bash() { - until /$/ { - yield other; - - if /#.*/ { - yield comment; - } else if /'/ { - until /$/ { - yield string; - if /'/ { yield string; break; } - await input; - } - } else if /"/ { - until /$/ { - yield string; - if /\\./ {} - else if /"/ { yield string; break; } - await input; - } - } else if /`/ { - until /$/ { - yield string; - if /\\./ {} - else if /`/ { yield string; break; } - await input; - } - } else if /\$\{[^}]+\}|\$[A-Za-z_]\w*|\$\d+|\$[#?*!@$-]/ { - yield variable; - } else if /(?:if|then|elif|else|fi|for|while|until|do|done|case|esac|in|select)\>/ { - if /\w+/ { - yield other; - } else { - yield keyword.control; - } - } else if /(?:declare|export|function|local|readonly|source)\>/ { - if /\w+/ { - yield other; - } else { - yield keyword.other; - } - } else if /-?(?:(?i:0x[\da-fA-F]+)|\d+\.?\d*|\.\d+)(?i:e[+-]?\d+)?/ { - if /\w+/ { - yield other; - } else { - yield constant.numeric; - } - } else if /([A-Za-z_][\w-]*)\s*\(/ { - yield $1 as method; - } else if /[A-Za-z_][\w-]*/ { - // Gobble any other tokens that should not be highlighted - } - - yield other; - } -} diff --git a/crates/lsh/definitions/javascript.lsh b/crates/lsh/definitions/javascript.lsh index 8c496a2278e..ca28a8062c3 100644 --- a/crates/lsh/definitions/javascript.lsh +++ b/crates/lsh/definitions/javascript.lsh @@ -39,34 +39,22 @@ pub fn javascript() { else if /`/ { yield string; break; } await input; } - } else if /(?:if|else|switch|case|default|for|while|do|break|continue|try|catch|finally|throw|return)\>/ { - if /\w+/ { - yield other; - } else { - yield keyword.control; - } - } else if /(?:async|await|class|const|delete|export|extends|function|import|in|instanceof|let|new|of|super|this|typeof|var|void|yield)\>/ { - if /\w+/ { - yield other; - } else { - yield keyword.other; - } + } else if /(?:break|case|catch|continue|debugger|default|do|else|finally|for|if|return|switch|throw|try|while)\>/ { + yield keyword.control; + } else if /(?:async|await|class|const|delete|export|extends|function|import|in|instanceof|let|new|of|static|super|this|typeof|var|void|yield)\>/ { + yield keyword.other; } else if /(?:true|false|null|undefined|NaN|Infinity)\>/ { + yield constant.language; + } else if /(?i:-?(?:0x[\da-fA-F_]+|0b[01_]+|0o[0-7_]+|[\d_]+\.?[\d_]*|\.[\d_]+)(?:e[+-]?[\d_]+)?)n?/ { if /\w+/ { - yield other; - } else { - yield constant.language; - } - } else if /-?(?:(?i:0x[\da-fA-F]+)|(?i:0b[01]+)|(?i:0o[0-7]+)|\d+\.?\d*|\.\d+)(?i:e[+-]?\d+)?/ { - if /\w+/ { - yield other; + // Invalid numeric literal } else { yield constant.numeric; } - } else if /([A-Za-z_$][\w$]*)\s*\(/ { + } else if /(\w+)\s*\(/ { yield $1 as method; - } else if /[A-Za-z_$][\w$]*/ { - // Gobble any other tokens that should not be highlighted + } else if /\w+/ { + // Gobble word chars to align the next iteration on a word boundary. } yield other; diff --git a/crates/lsh/definitions/markdown.lsh b/crates/lsh/definitions/markdown.lsh index fdb43a2e770..77a005fe925 100644 --- a/crates/lsh/definitions/markdown.lsh +++ b/crates/lsh/definitions/markdown.lsh @@ -18,23 +18,13 @@ pub fn markdown() { yield comment; } else if /```/ { // NOTE: These checks are sorted alphabetically. - if /(?i:bash|sh|shell)/ { + if /(?i:sh|bash)/ { loop { await input; if /\s*```/ { return; } else { - bash(); - if /.*/ {} - } - } - } else if /(?i:zsh)/ { - loop { - await input; - if /\s*```/ { - return; - } else { - zsh(); + shellscript(); if /.*/ {} } } @@ -50,7 +40,7 @@ pub fn markdown() { if /.*/ {} } } - } else if /(?i:javascript|js|jsx|mjs|cjs)/ { + } else if /(?i:javascript|js)/ { loop { await input; if /\s*```/ { @@ -70,7 +60,7 @@ pub fn markdown() { if /.*/ {} } } - } else if /(?i:py|python)/ { + } else if /(?i:py)/ { loop { await input; if /\s*```/ { diff --git a/crates/lsh/definitions/python.lsh b/crates/lsh/definitions/python.lsh index becaed7fb89..9d1ebb6c533 100644 --- a/crates/lsh/definitions/python.lsh +++ b/crates/lsh/definitions/python.lsh @@ -34,36 +34,24 @@ pub fn python() { else if /"/ { yield string; break; } await input; } - } else if /(?:if|elif|else|for|while|try|except|finally|return|break|continue|raise|with|match|case|pass)\>/ { - if /\w+/ { - yield other; - } else { - yield keyword.control; - } - } else if /(?:and|as|assert|async|await|class|def|del|from|global|import|in|is|lambda|nonlocal|not|or|yield)\>/ { - if /\w+/ { - yield other; - } else { - yield keyword.other; - } + } else if /(?:break|case|continue|elif|else|except|finally|for|if|match|pass|raise|return|try|while|with)\>/ { + yield keyword.control; + } else if /(?:and|assert|async|as|await|class|def|del|from|global|import|in|is|lambda|nonlocal|not|or|type|yield)\>/ { + yield keyword.other; } else if /(?:True|False|None)\>/ { + yield constant.language; + } else if /(?i:-?(?:0x[\da-fA-F_]+|0b[01_]+|0o[0-7_]+|[\d_]+\.?[\d_]*|\.[\d_]+)(?:e[+-]?[\d_]+)?j?)/ { if /\w+/ { - yield other; - } else { - yield constant.language; - } - } else if /-?(?:(?i:0x[\da-fA-F]+)|(?i:0b[01]+)|(?i:0o[0-7]+)|\d+\.?\d*|\.\d+)(?i:e[+-]?\d+)?(?i:j)?/ { - if /\w+/ { - yield other; + // Invalid numeric literal } else { yield constant.numeric; } - } else if /@[A-Za-z_]\w*/ { + } else if /@\w+/ { yield markup.link; - } else if /([A-Za-z_]\w*)\s*\(/ { + } else if /(\w+)\s*\(/ { yield $1 as method; - } else if /[A-Za-z_]\w*/ { - // Gobble any other tokens that should not be highlighted + } else if /\w+/ { + // Gobble word chars to align the next iteration on a word boundary. } yield other; diff --git a/crates/lsh/definitions/shellscript.lsh b/crates/lsh/definitions/shellscript.lsh new file mode 100644 index 00000000000..50da98186e1 --- /dev/null +++ b/crates/lsh/definitions/shellscript.lsh @@ -0,0 +1,87 @@ +#[display_name = "Shell Script"] +#[path = "**/*.sh"] +#[path = "**/*.bash"] +#[path = "**/*.bashrc"] +#[path = "**/*.bash_profile"] +#[path = "**/*.profile"] +#[path = "**/*.zsh"] +#[path = "**/*.zshrc"] +#[path = "**/APKBUILD"] +#[path = "**/PKGBUILD"] +#[path = "**/zshrc"] +pub fn shellscript() { + until /$/ { + yield other; + + if /#.*/ { + yield comment; + } else if /'/ { + until /$/ { + yield string; + if /'/ { yield string; break; } + await input; + } + } else if /\$'/ { + // ANSI C quoting $'...\n...' + until /$/ { + yield string; + if /\\./ {} + else if /'/ { yield string; break; } + await input; + } + } else if /"/ { + // TODO: Handle nested evaluation via $(...) and $((...)). + until /$/ { + yield string; + if /\\./ {} + else if /\$(?:\{[^}]+\}|\(\([^)]*\)\)|\([^)]*\)|[A-Za-z_]\w*|\d+|[#?*!@$-])/ { + yield variable; + } + else if /"/ { yield string; break; } + await input; + } + } else if /`/ { + until /$/ { + yield string; + if /\\./ {} + else if /`/ { yield string; break; } + await input; + } + } else if /<<-?\s*['"]?(?:EOF|END|HEREDOC)\>['"]?.*/ { + // TODO: We need proper capturing to support arbitrary delimiters + lookbehind across lines. + yield other; + loop { + await input; + if /\t*(?:EOF|END|HEREDOC)\>.*/ { + yield other; + break; + } + if /.*/ {} + yield string; + } + } else if /\$(?:\{[^}]+\}|\(\([^)]*\)\)|\([^)]*\)|[A-Za-z_]\w*|\d+|[#?*!@$-])/ { + // $var, ${var:-default}, $(cmd), $((1+2)), etc. + yield variable; + } else if /(?:break|case|continue|done|do|elif|else|esac|fi|for|if|in|return|select|then|time|until|while)\>/ { + yield keyword.control; + } else if /function\>/ { + yield keyword.other; + if /\s+(\w+)/ { + yield $1 as method; + } + } else if /(\w+)\s*\(/ { + yield $1 as method; + } else if /(?:alias|declare|export|local|readonly|source|typeset)\>/ { + yield keyword.other; + } else if /(?:true|false)\>/ { + yield constant.language; + } else if /(?:(?i:0x[\da-fA-F]+)|-?\d+(?:\.\d+)?)\>/ { + // NOTE: The somewhat niche base#value notation (e.g. 2#1010) is missing. + yield constant.numeric; + } else if /\w+/ { + // Gobble word chars to align the next iteration on a word boundary. + } + + yield other; + } +} diff --git a/crates/lsh/definitions/zsh.lsh b/crates/lsh/definitions/zsh.lsh deleted file mode 100644 index f22a4fff245..00000000000 --- a/crates/lsh/definitions/zsh.lsh +++ /dev/null @@ -1,60 +0,0 @@ -#[display_name = "Zsh"] -#[path = "**/*.zsh"] -#[path = "**/.zprofile"] -#[path = "**/.zshenv"] -#[path = "**/.zshrc"] -pub fn zsh() { - until /$/ { - yield other; - - if /#.*/ { - yield comment; - } else if /'/ { - until /$/ { - yield string; - if /'/ { yield string; break; } - await input; - } - } else if /"/ { - until /$/ { - yield string; - if /\\./ {} - else if /"/ { yield string; break; } - await input; - } - } else if /`/ { - until /$/ { - yield string; - if /\\./ {} - else if /`/ { yield string; break; } - await input; - } - } else if /\$\{[^}]+\}|\$[A-Za-z_]\w*|\$\d+|\$[#?*!@$-]/ { - yield variable; - } else if /(?:if|then|elif|else|fi|for|while|until|do|done|case|esac|in|select)\>/ { - if /\w+/ { - yield other; - } else { - yield keyword.control; - } - } else if /(?:alias|autoload|bindkey|compdef|declare|emulate|export|function|local|readonly|setopt|source|typeset|unalias|unsetopt|zmodload)\>/ { - if /\w+/ { - yield other; - } else { - yield keyword.other; - } - } else if /-?(?:(?i:0x[\da-fA-F]+)|\d+\.?\d*|\.\d+)(?i:e[+-]?\d+)?/ { - if /\w+/ { - yield other; - } else { - yield constant.numeric; - } - } else if /([A-Za-z_][\w-]*)\s*\(/ { - yield $1 as method; - } else if /[A-Za-z_][\w-]*/ { - // Gobble any other tokens that should not be highlighted - } - - yield other; - } -}