diff --git a/assets/highlighting-tests/bash.sh b/assets/highlighting-tests/bash.sh deleted file mode 100644 index dfd5238724db..000000000000 --- 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 new file mode 100644 index 000000000000..81323e48d7da --- /dev/null +++ b/assets/highlighting-tests/javascript.js @@ -0,0 +1,117 @@ +// Comments +// Single-line comment + +/* + * Multi-line + * comment + */ + +// 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 Animal extends Object { + static count = 0; + + constructor(name) { + super(); + this.name = name; + Animal.count++; + } + + 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/markdown.md b/assets/highlighting-tests/markdown.md index c4845eb68014..555b182f9618 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 000000000000..03dcf0d90be4 --- /dev/null +++ b/assets/highlighting-tests/python.py @@ -0,0 +1,142 @@ +# Comments +# Single-line comment + +# 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 + + 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 + +# 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 000000000000..d263b88d0fd4 --- /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/crates/lsh/definitions/javascript.lsh b/crates/lsh/definitions/javascript.lsh new file mode 100644 index 000000000000..ca28a8062c34 --- /dev/null +++ b/crates/lsh/definitions/javascript.lsh @@ -0,0 +1,62 @@ +#[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 /(?: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+/ { + // Invalid numeric literal + } else { + yield constant.numeric; + } + } else if /(\w+)\s*\(/ { + yield $1 as method; + } 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 3fcc7721a157..77a005fe9252 100644 --- a/crates/lsh/definitions/markdown.lsh +++ b/crates/lsh/definitions/markdown.lsh @@ -18,7 +18,17 @@ pub fn markdown() { yield comment; } else if /```/ { // NOTE: These checks are sorted alphabetically. - if /(?i:diff)/ { + if /(?i:sh|bash)/ { + loop { + await input; + if /\s*```/ { + return; + } else { + shellscript(); + if /.*/ {} + } + } + } else if /(?i:diff)/ { loop { await input; if /\s*```/ { @@ -30,6 +40,16 @@ pub fn markdown() { if /.*/ {} } } + } else if /(?i:javascript|js)/ { + loop { + await input; + if /\s*```/ { + return; + } else { + javascript(); + if /.*/ {} + } + } } else if /(?i:json)/ { loop { await input; @@ -40,6 +60,16 @@ pub fn markdown() { if /.*/ {} } } + } else if /(?i:py)/ { + 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 000000000000..9d1ebb6c5334 --- /dev/null +++ b/crates/lsh/definitions/python.lsh @@ -0,0 +1,59 @@ +#[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 /(?: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+/ { + // Invalid numeric literal + } else { + yield constant.numeric; + } + } else if /@\w+/ { + yield markup.link; + } else if /(\w+)\s*\(/ { + yield $1 as method; + } 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 000000000000..50da98186e14 --- /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; + } +}