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 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. */
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)

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,
UnexpectedEndOfInputError
} from "../parse.js";
import {ElementType} from "../../util/types.js";
export type StrLVal = { term: 'variable', name: string }
@ -48,7 +49,12 @@ export class ParseContext {
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 ) {
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) ) {
throw new IsNotKeywordError('Unexpected term: ' + input.value + ' (expected one of: ' + options.join(', ') + ')')
}
return { term: 'string', value: input.value as ElementType<T> }
}
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 {Save} from "./save.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 const commands: Commands = [
new Clear,
new Contains,
new Copy,
new Edit,
new Enclose,
new Exit,
new From,
new Help,
new History,
new Indent,
new InFile,
new Join,
new Lines,
new Lipsum,
new Load,
new Lower,
new LSub,
new Missing,
new OutFile,
new Paste,
new Prefix,
new Quote,
new Redo,
new Replace,
new RSub,
new RunFile,
new Save,
new Show,
new Split,
new Suffix,
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 {LexInput} from '../lexer.js'
export class Lipsum extends Command<{ length: StrTerm }> {
attemptParse(context: ParseContext): { length: StrTerm } {
export type LipsumData = { length: StrTerm, type: 'word'|'line'|'para' }
export class Lipsum extends Command<LipsumData> {
attemptParse(context: ParseContext): LipsumData {
return {
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'
}
}