From 01f1f7ae227af29abcac7556e62563bdedc97583 Mon Sep 17 00:00:00 2001 From: KernelDeimos Date: Wed, 24 May 2023 17:40:51 -0400 Subject: [PATCH] Add tab completion --- src/ansi-shell/readline.js | 66 +++++++++++++++++++- src/ansi-shell/rl_comprehend.js | 2 + src/puter-shell/PuterANSIShell.js | 11 +++- src/puter-shell/completers/file_completer.js | 31 +++++++++ 4 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 src/puter-shell/completers/file_completer.js diff --git a/src/ansi-shell/readline.js b/src/ansi-shell/readline.js index 01ee60f..a0877d8 100644 --- a/src/ansi-shell/readline.js +++ b/src/ansi-shell/readline.js @@ -1,4 +1,5 @@ import { Context } from "../context/context"; +import { FileCompleter } from "../puter-shell/completers/file_completer"; import { Uint8List } from "../util/bytes"; import { Log } from "../util/log"; import { StatefulProcessorBuilder } from "../util/statemachine"; @@ -34,6 +35,7 @@ const ReadlineProcessorBuilder = builder => builder .external('in_', { required: true }) .external('history', { required: true }) .external('prompt', { required: true }) + .external('commandCtx', { required: true }) .beforeAll('get-byte', async ctx => { const { locals, externs } = ctx; @@ -72,6 +74,65 @@ const ReadlineProcessorBuilder = builder => builder })); // NEXT: get tab completer for input state console.log('input state', inputState); + + let completer = null; + if ( inputState.$ === 'redirect' ) { + completer = new FileCompleter(); + } + + // TODO: try to get a completer from the command + if ( inputState.$ === 'command' ) { + completer = new FileCompleter(); + } + + if ( completer === null ) return; + + const completions = await completer.getCompetions( + externs.commandCtx, + inputState, + ); + + const applyCompletion = txt => { + const p1 = vars.result.slice(0, vars.cursor); + const p2 = vars.result.slice(vars.cursor); + console.log({ p1, p2 }); + vars.result = p1 + txt + p2; + vars.cursor += txt.length; + externs.out.write(txt); + }; + + if ( completions.length === 0 ) return; + + if ( completions.length === 1 ) { + applyCompletion(completions[0]); + } + + if ( completions.length > 1 ) { + let inCommon = ''; + for ( let i=0 ; true ; i++ ) { + if ( ! completions.every(completion => { + return completion.length > i; + }) ) break; + + let matches = true; + + const chrFirst = completions[0][i]; + for ( let ci=1 ; ci < completions.length ; ci++ ) { + const chrOther = completions[ci][i]; + if ( chrFirst !== chrOther ) { + matches = false; + break; + } + } + + if ( ! matches ) break; + inCommon += chrFirst; + } + + if ( inCommon.length > 0 ) { + applyCompletion(inCommon); + } + } return; } @@ -239,7 +300,7 @@ class Readline { this.history = new HistoryManager(); } - async readline (prompt) { + async readline (prompt, commandCtx) { const out = this.internal_.out; const in_ = this.internal_.in; @@ -250,7 +311,8 @@ class Readline { } = await ReadlineProcessor.run({ prompt, out, in_, - history: this.history + history: this.history, + commandCtx, }); this.history.save(result); diff --git a/src/ansi-shell/rl_comprehend.js b/src/ansi-shell/rl_comprehend.js index 1b68083..a2c1263 100644 --- a/src/ansi-shell/rl_comprehend.js +++ b/src/ansi-shell/rl_comprehend.js @@ -108,6 +108,8 @@ export const readline_comprehend = (ctx) => { $: 'command', id: tokens[0], tokens: argTokens, + input: endsWithWhitespace ? + '' : argTokens[argTokens.length - 1], endsWithWhitespace, }; }; diff --git a/src/puter-shell/PuterANSIShell.js b/src/puter-shell/PuterANSIShell.js index 4a87f38..b4c55ab 100644 --- a/src/puter-shell/PuterANSIShell.js +++ b/src/puter-shell/PuterANSIShell.js @@ -62,8 +62,17 @@ export class PuterANSIShell extends EventTarget { async doPromptIteration() { console.log('prompt iteration'); const { readline } = this.ctx.externs; + // DRY: created the same way in runPipeline + const executionCtx = this.ctx.sub({ + vars: this.variables, + env: this.env, + locals: { + pwd: this.variables.pwd, + } + }); const input = await readline( - this.expandPromptString(this.env.PS1) + this.expandPromptString(this.env.PS1), + executionCtx, ); await this.runPipeline(input); diff --git a/src/puter-shell/completers/file_completer.js b/src/puter-shell/completers/file_completer.js new file mode 100644 index 0000000..30cefd3 --- /dev/null +++ b/src/puter-shell/completers/file_completer.js @@ -0,0 +1,31 @@ +import path_ from "path-browserify"; + +// DRY: also done in many other places +const resolve = (ctx, relPath) => { + if ( relPath.startsWith('/') ) { + return relPath; + } + console.log('wrong context?', ctx); + return path_.resolve(ctx.vars.pwd, relPath); +} + +export class FileCompleter { + async getCompetions (ctx, inputState) { + const { puterShell } = ctx.externs; + + let path = resolve(ctx, inputState.input); + let dir = path_.dirname(path); + let base = path_.basename(path); + + const completions = []; + + const result = await puterShell.command('list', { path: dir }); + for ( const item of result ) { + if ( item.name.startsWith(base) ) { + completions.push(item.name.slice(base.length)); + } + } + + return completions; + } +}