import {parser} from "./parser.js" import { LRLanguage, LanguageSupport, indentNodeProp, foldNodeProp, foldInside, delimitedIndent, continuedIndent } from "@codemirror/language" import {styleTags, tags as t} from "@lezer/highlight" import {parseMixed} from "@lezer/common" import {completeFromList, CompletionSource} from "@codemirror/autocomplete" import {cshLanguage, cshCompletion} from "./csh.js" import {fishLanguage, fishCompletion} from "./fish.js" // A backtick substitution holds commands, but its opening and closing // characters are the same one, so the grammar reads it as a single token and // the commands are parsed here instead, by handing the range between the // backticks back to this parser. The reference is resolved when a parse asks // for it, which is after the assignment below has run. const insideBackticks = parseMixed(node => node.name == "Backtick" && node.to - node.from > 2 ? {parser: shellParser, overlay: [{from: node.from + 1, to: node.to - 1}]} : null ) const shellParser = parser.configure({ wrap: insideBackticks, props: [ indentNodeProp.add({ Group: delimitedIndent({closing: "}"}), Subshell: delimitedIndent({closing: ")"}), ArrayLiteral: delimitedIndent({closing: ")"}), CommandSubstitution: delimitedIndent({closing: ")"}), DoGroup: delimitedIndent({closing: "done"}), CaseStatement: delimitedIndent({closing: "esac"}), // `then`, `elif` and `else` sit on the same line as the `if` they // belong to, so what is indented is everything after that line. IfStatement: continuedIndent({except: /^\s*(fi|elif|else)\b/}), CaseClause: continuedIndent({except: /^\s*;;/}) }), foldNodeProp.add({ Group: foldInside, Subshell: foldInside, ArrayLiteral: foldInside, CommandSubstitution: foldInside, DoGroup: foldInside, CaseStatement: foldInside }), styleTags({ "if then elif else fi": t.controlKeyword, "for while until do done in select": t.controlKeyword, "case esac": t.controlKeyword, function: t.definitionKeyword, Comment: t.lineComment, String: t.string, RawString: t.string, AnsiString: t.special(t.string), HeredocBody: t.special(t.string), Escape: t.escape, LineContinuation: t.escape, Glob: t.special(t.string), GlobQualifier: t.special(t.string), Regex: t.regexp, // A command name reads as the call it is. Shell declares nothing, so // every other word is text until something expands it. "SimpleCommand/CommandName": t.function(t.variableName), "FunctionDefinition/FunctionName": t.function(t.definition(t.variableName)), VariableName: t.definition(t.variableName), AssignName: t.definition(t.variableName), SimpleExpansion: t.variableName, ParamName: t.variableName, SpecialParam: t.special(t.variableName), "ParamOp ExpansionFlags": t.operator, Dollar: t.variableName, ArithVariable: t.variableName, ArithNumber: t.number, // Everything that decides what runs next, rather than what it runs on. RedirectOp: t.controlOperator, "PipeOp Background": t.controlOperator, "AndOp OrOp NotOp": t.logicOperator, CaseTerminator: t.punctuation, Terminator: t.separator, "CondOp CondRegexOp": t.compareOperator, "ArithPowerOp ArithMulOp ArithAddOp": t.arithmeticOperator, "ArithShiftOp ArithBitAndOp ArithBitXorOp ArithBitOrOp ArithNotOp": t.bitwiseOperator, "ArithRelateOp ArithEqualOp": t.compareOperator, "ArithAndOp ArithOrOp": t.logicOperator, ArithIncDecOp: t.updateOperator, ArithAssignOp: t.definitionOperator, // The quotes belong to the string they open, so that a string reads as // one thing even where it holds an expansion. '"\\""': t.string, '"${" "}" "{"': t.brace, '"$((" "$(" "((" "(" ")"': t.paren, '"[[" "]]" "[" "]"': t.squareBracket, '";" "," "?" ":"': t.separator }) ] }) /// A language provider based on the Lezer shell parser in this package, /// extended with highlighting, folding and indentation information. export const shellLanguage = LRLanguage.define({ name: "shell", parser: shellParser, languageData: { commentTokens: {line: "#"}, closeBrackets: {brackets: ["(", "[", "{", "'", '"', "`"]}, indentOnInput: /^\s*(fi|done|esac|else|elif|\}|\)|;;)$/ } }) const keywords = "if then elif else fi for while until do done case esac in function select time" .split(" ") .map(label => ({label, type: "keyword"})) // The shell builtins. These are the commands that change the shell itself // rather than start a program, which is the half of the language a `man` page // will not find for you. const builtins = [ "alias", "bg", "bind", "break", "builtin", "cd", "command", "continue", "declare", "dirs", "disown", "echo", "enable", "eval", "exec", "exit", "export", "false", "fc", "fg", "getopts", "hash", "help", "history", "jobs", "kill", "let", "local", "logout", "mapfile", "popd", "printf", "pushd", "pwd", "read", "readarray", "readonly", "return", "set", "shift", "shopt", "source", "suspend", "test", "times", "trap", "true", "type", "typeset", "ulimit", "umask", "unalias", "unset", "wait" ].map(label => ({label, type: "function"})) // The variables the shell sets for you, which are impossible to guess from the // language itself. const specials = [ "BASH", "BASH_VERSION", "BASHPID", "EUID", "FUNCNAME", "GROUPS", "HOME", "HOSTNAME", "IFS", "LANG", "LINENO", "OLDPWD", "OPTARG", "OPTIND", "PATH", "PIPESTATUS", "PPID", "PS1", "PS2", "PWD", "RANDOM", "REPLY", "SECONDS", "SHELL", "SHLVL", "TMPDIR", "UID", "USER" ].map(label => ({label, type: "constant"})) /// Autocompletion for the shell's reserved words, builtin commands and the /// variables it sets. export const shellCompletion: CompletionSource = completeFromList([ ...keywords, ...builtins, ...specials ]) // The shells that are not read by the grammar above, because neither is a // dialect of the shell command language: each has a grammar of its own in this // package and answers to a name like any other shell. const ownGrammars: {[shell: string]: {language: LRLanguage; completion: CompletionSource}} = { csh: {language: cshLanguage, completion: cshCompletion}, fish: {language: fishLanguage, completion: fishCompletion} } // The names an editor is likely to have to hand. sh, bash, zsh and ksh are one // grammar, the way this package's AWK grammar is one grammar for awk and gawk: // somebody reading a script does not always know which of them wrote it. const aliases: {[name: string]: string} = { sh: "sh", shell: "sh", bash: "sh", dash: "sh", ash: "sh", posix: "sh", zsh: "sh", ksh: "sh", ksh93: "sh", mksh: "sh", csh: "csh", tcsh: "csh", fish: "fish" } /// The name of a shell this package reads, or any of the aliases above. export type Shell = "sh" | "csh" | "fish" /// The language for one shell. `"csh"`, `"tcsh"` and `"fish"` are answered with /// their own grammars, and everything else with the shell command language. /// What a caller does with the result is the same either way. export function shellLanguageFor(shell: string): LRLanguage { const own = ownGrammars[aliases[shell.toLowerCase()] ?? shell.toLowerCase()] return own ? own.language : shellLanguage } export interface ShellConfig { /// Whose shell to read. `"sh"`, `"bash"`, `"zsh"` and `"ksh"` are all the one /// grammar; `"csh"`, `"tcsh"` and `"fish"` each have their own. Defaults to /// `"sh"`. shell?: Shell | string } /// Shell language support, with completion for the builtins and the variables /// the shell sets. export function shell(config: ShellConfig = {}) { const own = ownGrammars[aliases[(config.shell ?? "sh").toLowerCase()] ?? ""] const language = own ? own.language : shellLanguage const completion = own ? own.completion : shellCompletion return new LanguageSupport(language, [language.data.of({autocomplete: completion})]) } /// C shell support, for a caller who knows which of the grammars they want. export function csh() { return shell({shell: "csh"}) } /// fish support, likewise. export function fish() { return shell({shell: "fish"}) } export {parser} export {cshLanguage, cshCompletion, cshParser} from "./csh.js" export {fishLanguage, fishCompletion, fishParser} from "./fish.js"