Implement sub-command parsing + add on/word/line/over commands

This commit is contained in:
2025-11-11 22:09:26 -06:00
parent bfc9459b69
commit aaff8a5011
8 changed files with 153 additions and 9 deletions

View File

@@ -4,7 +4,13 @@ import {StreamLogger} from '../util/log.js'
import {log} from '../log.js'
import {Commands} from './commands/index.js'
import {Command, CommandData, ParseContext} from './commands/command.js'
import {Executable, InternalParseError, InvalidCommandError, IsNotKeywordError} from './parse.js'
import {
Executable,
InternalParseError,
InvalidCommandError,
IsNotKeywordError,
UnexpectedEndOfInputError
} from './parse.js'
export class Parser extends BehaviorSubject<Executable<CommandData>> {
private logger: StreamLogger
@@ -12,10 +18,10 @@ export class Parser extends BehaviorSubject<Executable<CommandData>> {
private parseCandidate?: Command<CommandData>
private inputForCandidate: LexInput[] = []
constructor(lexer: Lexer, private commands: Commands) {
constructor(private commands: Commands, lexer?: Lexer) {
super()
this.logger = log.getStreamLogger('parser')
lexer.subscribe(token => this.handleToken(token))
lexer?.subscribe(token => this.handleToken(token))
}
async handleToken(token: LexToken) {
@@ -46,9 +52,9 @@ export class Parser extends BehaviorSubject<Executable<CommandData>> {
if ( token.type === 'terminator' ) {
try {
// Have the candidate attempt to parse itself from the collecte data:
const context = new ParseContext(this.inputForCandidate)
const context = this.getContext()
this.logger.verbose({ parsing: this.parseCandidate.getDisplayName(), context })
const data = this.parseCandidate.attemptParse(context)
const data = await this.parseCandidate.attemptParse(context)
// The candidate must consume every token in the context:
context.assertEmpty()
@@ -91,4 +97,28 @@ export class Parser extends BehaviorSubject<Executable<CommandData>> {
return `(${token.literal ? 'LITERAL' : 'INPUT'}) ${token.value}`
}
private getContext(): ParseContext {
return new ParseContext(
this.inputForCandidate,
async tokens => {
const childParser = new Parser(this.commands)
while ( !childParser.currentValue ) {
if ( !tokens.length ) {
await childParser.handleToken({ type: 'terminator' })
break
}
await childParser.handleToken(tokens.shift()!)
}
const parsedExecutable = childParser.currentValue
if ( !parsedExecutable ) {
throw new UnexpectedEndOfInputError('Unable to parse child command: unexpected end of tokens')
}
return [parsedExecutable, tokens]
}
)
}
}