Add & register more basic commands

This commit is contained in:
Garrett Mills 2025-11-11 21:37:32 -06:00
parent c437958406
commit bfc9459b69
27 changed files with 514 additions and 3 deletions

View File

@ -3,6 +3,8 @@ export type Awaitable<T> = T | Promise<T>
export type JSONScalar = string | boolean | number | undefined export type JSONScalar = string | boolean | number | undefined
export type JSONData = JSONScalar | Array<JSONScalar | JSONData> | { [key: string]: JSONScalar | JSONData } export type JSONData = JSONScalar | Array<JSONScalar | JSONData> | { [key: string]: JSONScalar | JSONData }
export type ElementType<T extends readonly any[]> = T extends (infer U)[] ? U : never;
/** A typescript-compatible version of Object.hasOwnProperty. */ /** A typescript-compatible version of Object.hasOwnProperty. */
export function hasOwnProperty<X extends {}, Y extends PropertyKey>(obj: X, prop: Y): obj is X & Record<Y, unknown> { // eslint-disable-line @typescript-eslint/ban-types export function hasOwnProperty<X extends {}, Y extends PropertyKey>(obj: X, prop: Y): obj is X & Record<Y, unknown> { // eslint-disable-line @typescript-eslint/ban-types
return Object.hasOwnProperty.call(obj, prop) return Object.hasOwnProperty.call(obj, prop)

16
src/vm/commands/clear.ts Normal file
View File

@ -0,0 +1,16 @@
import {Command, ParseContext} from "./command.js";
import {LexInput} from "../lexer.js";
export class Clear extends Command<{}> {
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'clear')
}
attemptParse(context: ParseContext): {} {
return {}
}
getDisplayName(): string {
return 'clear'
}
}

View File

@ -5,6 +5,7 @@ import {
IsNotKeywordError, IsNotKeywordError,
UnexpectedEndOfInputError UnexpectedEndOfInputError
} from "../parse.js"; } from "../parse.js";
import {ElementType} from "../../util/types.js";
export type StrLVal = { term: 'variable', name: string } export type StrLVal = { term: 'variable', name: string }
@ -48,7 +49,12 @@ export class ParseContext {
return { term: 'string', value: input.value, literal: input.literal } return { term: 'string', value: input.value, literal: input.literal }
} }
popKeywordInSet<T extends string[]>(options: T) { popOptionalKeywordInSet<const T extends readonly string[]>(options: T): (StrTerm & { value: ElementType<T> }) | undefined {
if ( this.inputs.length ) return this.popKeywordInSet(options)
return undefined
}
popKeywordInSet<const T extends readonly string[]>(options: T): StrTerm & { value: ElementType<T> } {
if ( !this.inputs.length ) { if ( !this.inputs.length ) {
throw new UnexpectedEndOfInputError('Unexpected end of input. Expected one of: ' + options.join(', ')) throw new UnexpectedEndOfInputError('Unexpected end of input. Expected one of: ' + options.join(', '))
} }
@ -58,6 +64,8 @@ export class ParseContext {
if ( input.literal || !options.includes(input.value) ) { if ( input.literal || !options.includes(input.value) ) {
throw new IsNotKeywordError('Unexpected term: ' + input.value + ' (expected one of: ' + options.join(', ') + ')') throw new IsNotKeywordError('Unexpected term: ' + input.value + ' (expected one of: ' + options.join(', ') + ')')
} }
return { term: 'string', value: input.value as ElementType<T> }
} }
popLVal(): StrLVal { popLVal(): StrLVal {

View File

@ -0,0 +1,18 @@
import { LexInput } from "../lexer.js";
import {Command, ParseContext, StrTerm} from "./command.js";
export class Contains extends Command<{ find: StrTerm }> {
attemptParse(context: ParseContext): { find: StrTerm } {
return {
find: context.popTerm(),
}
}
getDisplayName(): string {
return 'contains'
}
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'contains')
}
}

View File

@ -0,0 +1,24 @@
import {Command, ParseContext, StrTerm} from "./command.js";
import {LexInput} from "../lexer.js";
export type EncloseData = {
left?: StrTerm,
right?: StrTerm,
}
export class Enclose extends Command<EncloseData> {
attemptParse(context: ParseContext): EncloseData {
return {
left: context.popOptionalTerm(),
right: context.popOptionalTerm(),
}
}
getDisplayName(): string {
return 'enclose'
}
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'enclose')
}
}

16
src/vm/commands/help.ts Normal file
View File

@ -0,0 +1,16 @@
import {Command, ParseContext} from "./command.js";
import {LexInput} from "../lexer.js";
export class Help extends Command<{}> {
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'help')
}
attemptParse(context: ParseContext): {} {
return {}
}
getDisplayName(): string {
return 'help'
}
}

24
src/vm/commands/indent.ts Normal file
View File

@ -0,0 +1,24 @@
import {Command, ParseContext, StrTerm} from "./command.js";
import {LexInput} from "../lexer.js";
export type IndentData = {
type: 'space'|'tab',
level?: StrTerm,
}
export class Indent extends Command<IndentData> {
attemptParse(context: ParseContext): IndentData {
return {
type: context.popKeywordInSet(['space', 'tab']).value,
level: context.popOptionalTerm(),
}
}
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'indent')
}
getDisplayName(): string {
return 'indent'
}
}

View File

@ -11,19 +11,67 @@ import {Paste} from "./paste.js";
import {RunFile} from "./runfile.js"; import {RunFile} from "./runfile.js";
import {Save} from "./save.js"; import {Save} from "./save.js";
import {To} from "./to.js"; import {To} from "./to.js";
import {Lipsum} from "./lipsum.js";
import {Indent} from "./indent.js";
import {Clear} from "./clear.js";
import {Contains} from "./contains.js";
import {Enclose} from "./enclose.js";
import {Help} from "./help.js";
import {Join} from "./join.js";
import {Lines} from "./lines.js";
import {Lower} from "./lower.js";
import {LSub} from "./lsub.js";
import {Missing} from "./missing.js";
import {Prefix} from "./prefix.js";
import {Quote} from "./quote.js";
import {Redo} from "./redo.js";
import {Replace} from "./replace.js";
import {RSub} from "./rsub.js";
import {Show} from "./show.js";
import {Split} from "./split.js";
import {Suffix} from "./suffix.js";
import {Trim} from "./trim.js";
import {Undo} from "./undo.js";
import {Unique} from "./unique.js";
import {Unquote} from "./unquote.js";
import {Upper} from "./upper.js";
export type Commands = Command<CommandData>[] export type Commands = Command<CommandData>[]
export const commands: Commands = [ export const commands: Commands = [
new Clear,
new Contains,
new Copy, new Copy,
new Edit, new Edit,
new Enclose,
new Exit, new Exit,
new From, new From,
new Help,
new History, new History,
new Indent,
new InFile, new InFile,
new Join,
new Lines,
new Lipsum,
new Load, new Load,
new Lower,
new LSub,
new Missing,
new OutFile, new OutFile,
new Paste, new Paste,
new Prefix,
new Quote,
new Redo,
new Replace,
new RSub,
new RunFile, new RunFile,
new Save, new Save,
new Show,
new Split,
new Suffix,
new To, new To,
new Trim,
new Undo,
new Unique,
new Unquote,
new Upper,
] ]

18
src/vm/commands/join.ts Normal file
View File

@ -0,0 +1,18 @@
import {Command, ParseContext, StrTerm} from "./command.js";
import {LexInput} from "../lexer.js";
export class Join extends Command<{ with: StrTerm }> {
attemptParse(context: ParseContext): { with: StrTerm } {
return {
with: context.popTerm(),
}
}
getDisplayName(): string {
return 'join'
}
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'join')
}
}

24
src/vm/commands/lines.ts Normal file
View File

@ -0,0 +1,24 @@
import {Command, ParseContext, StrTerm} from "./command.js";
import {LexInput} from "../lexer.js";
export type LinesData = {
on?: StrTerm,
with?: StrTerm,
}
export class Lines extends Command<LinesData> {
attemptParse(context: ParseContext): LinesData {
return {
on: context.popOptionalTerm(),
with: context.popOptionalTerm(),
}
}
getDisplayName(): string {
return 'lines'
}
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'lines')
}
}

View File

@ -1,10 +1,13 @@
import {Command, ParseContext, StrTerm} from './command.js' import {Command, ParseContext, StrTerm} from './command.js'
import {LexInput} from '../lexer.js' import {LexInput} from '../lexer.js'
export class Lipsum extends Command<{ length: StrTerm }> { export type LipsumData = { length: StrTerm, type: 'word'|'line'|'para' }
attemptParse(context: ParseContext): { length: StrTerm } {
export class Lipsum extends Command<LipsumData> {
attemptParse(context: ParseContext): LipsumData {
return { return {
length: context.popTerm(), length: context.popTerm(),
type: context.popKeywordInSet(['word', 'line', 'para']).value,
} }
} }

16
src/vm/commands/lower.ts Normal file
View File

@ -0,0 +1,16 @@
import {Command, ParseContext} from "./command.js";
import {LexInput} from "../lexer.js";
export class Lower extends Command<{}> {
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'lower')
}
attemptParse(context: ParseContext): {} {
return {}
}
getDisplayName(): string {
return 'lower'
}
}

24
src/vm/commands/lsub.ts Normal file
View File

@ -0,0 +1,24 @@
import {Command, ParseContext, StrTerm} from "./command.js";
import {LexInput} from "../lexer.js";
export type LSubData = {
offset: StrTerm,
length?: StrTerm,
}
export class LSub extends Command<LSubData> {
attemptParse(context: ParseContext): LSubData {
return {
offset: context.popTerm(),
length: context.popOptionalTerm(),
}
}
getDisplayName(): string {
return 'lsub'
}
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'lsub')
}
}

View File

@ -0,0 +1,18 @@
import { LexInput } from "../lexer.js";
import {Command, ParseContext, StrTerm} from "./command.js";
export class Missing extends Command<{ find: StrTerm }> {
attemptParse(context: ParseContext): { find: StrTerm } {
return {
find: context.popTerm(),
}
}
getDisplayName(): string {
return 'missing'
}
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'missing')
}
}

18
src/vm/commands/prefix.ts Normal file
View File

@ -0,0 +1,18 @@
import {Command, ParseContext, StrTerm} from "./command.js";
import {LexInput} from "../lexer.js";
export class Prefix extends Command<{ with: StrTerm }> {
attemptParse(context: ParseContext): { with: StrTerm } {
return {
with: context.popTerm(),
}
}
getDisplayName(): string {
return 'prefix'
}
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'prefix')
}
}

18
src/vm/commands/quote.ts Normal file
View File

@ -0,0 +1,18 @@
import {Command, ParseContext, StrTerm} from "./command.js";
import {LexInput} from "../lexer.js";
export class Quote extends Command<{ with?: StrTerm }> {
attemptParse(context: ParseContext): { with?: StrTerm } {
return {
with: context.popOptionalTerm(),
}
}
getDisplayName(): string {
return 'quote'
}
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'quote')
}
}

18
src/vm/commands/redo.ts Normal file
View File

@ -0,0 +1,18 @@
import {Command, ParseContext, StrTerm} from "./command.js";
import {LexInput} from "../lexer.js";
export class Redo extends Command<{ steps: StrTerm }> {
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'redo')
}
attemptParse(context: ParseContext): { steps: StrTerm } {
return {
steps: context.popTerm(),
}
}
getDisplayName(): string {
return 'redo'
}
}

View File

@ -0,0 +1,24 @@
import {Command, ParseContext, StrTerm} from "./command.js";
import {LexInput} from "../lexer.js";
export type ReplaceData = {
find: StrTerm,
with: StrTerm,
}
export class Replace extends Command<ReplaceData> {
attemptParse(context: ParseContext): ReplaceData {
return {
find: context.popTerm(),
with: context.popTerm(),
}
}
getDisplayName(): string {
return 'replace'
}
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'replace')
}
}

24
src/vm/commands/rsub.ts Normal file
View File

@ -0,0 +1,24 @@
import {Command, ParseContext, StrTerm} from "./command.js";
import {LexInput} from "../lexer.js";
export type RSubData = {
offset: StrTerm,
length?: StrTerm,
}
export class RSub extends Command<RSubData> {
attemptParse(context: ParseContext): RSubData {
return {
offset: context.popTerm(),
length: context.popOptionalTerm(),
}
}
getDisplayName(): string {
return 'rsub'
}
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'rsub')
}
}

16
src/vm/commands/show.ts Normal file
View File

@ -0,0 +1,16 @@
import {Command, ParseContext} from "./command.js";
import {LexInput} from "../lexer.js";
export class Show extends Command<{}> {
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'show')
}
attemptParse(context: ParseContext): {} {
return {}
}
getDisplayName(): string {
return 'show'
}
}

24
src/vm/commands/split.ts Normal file
View File

@ -0,0 +1,24 @@
import {Command, ParseContext, StrTerm} from "./command.js";
import {LexInput} from "../lexer.js";
export type SplitData = {
on: StrTerm,
with?: StrTerm,
}
export class Split extends Command<SplitData> {
attemptParse(context: ParseContext): SplitData {
return {
on: context.popTerm(),
with: context.popOptionalTerm(),
}
}
getDisplayName(): string {
return 'split'
}
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'split')
}
}

18
src/vm/commands/suffix.ts Normal file
View File

@ -0,0 +1,18 @@
import {Command, ParseContext, StrTerm} from "./command.js";
import {LexInput} from "../lexer.js";
export class Suffix extends Command<{ with: StrTerm }> {
attemptParse(context: ParseContext): { with: StrTerm } {
return {
with: context.popTerm(),
}
}
getDisplayName(): string {
return 'suffix'
}
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'suffix')
}
}

24
src/vm/commands/trim.ts Normal file
View File

@ -0,0 +1,24 @@
import {Command, ParseContext, StrTerm} from "./command.js";
import {LexInput} from "../lexer.js";
export type TrimData = {
type?: 'start'|'end'|'both'|'left'|'right'|'lines',
char?: StrTerm,
}
export class Trim extends Command<TrimData> {
attemptParse(context: ParseContext): TrimData {
return {
type: context.popOptionalKeywordInSet(['start', 'end', 'both', 'left', 'right', 'lines'])?.value,
char: context.popOptionalTerm(),
}
}
getDisplayName(): string {
return 'trim'
}
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'trim')
}
}

18
src/vm/commands/undo.ts Normal file
View File

@ -0,0 +1,18 @@
import {Command, ParseContext, StrTerm} from "./command.js";
import {LexInput} from "../lexer.js";
export class Undo extends Command<{ steps: StrTerm }> {
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'undo')
}
attemptParse(context: ParseContext): { steps: StrTerm } {
return {
steps: context.popTerm(),
}
}
getDisplayName(): string {
return 'undo'
}
}

16
src/vm/commands/unique.ts Normal file
View File

@ -0,0 +1,16 @@
import {Command, ParseContext} from "./command.js";
import {LexInput} from "../lexer.js";
export class Unique extends Command<{}> {
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'unique')
}
attemptParse(context: ParseContext): {} {
return {}
}
getDisplayName(): string {
return 'unique'
}
}

View File

@ -0,0 +1,18 @@
import {Command, ParseContext, StrTerm} from "./command.js";
import {LexInput} from "../lexer.js";
export class Unquote extends Command<{ with?: StrTerm }> {
attemptParse(context: ParseContext): { with?: StrTerm } {
return {
with: context.popOptionalTerm(),
}
}
getDisplayName(): string {
return 'unquote'
}
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'unquote')
}
}

16
src/vm/commands/upper.ts Normal file
View File

@ -0,0 +1,16 @@
import {Command, ParseContext} from "./command.js";
import {LexInput} from "../lexer.js";
export class Upper extends Command<{}> {
isParseCandidate(token: LexInput): boolean {
return this.isKeyword(token, 'upper')
}
attemptParse(context: ParseContext): {} {
return {}
}
getDisplayName(): string {
return 'upper'
}
}