From 8043dfad1f11dda3d23cf2d1747ba6c3677fbdca Mon Sep 17 00:00:00 2001 From: Stephan Kast Date: Tue, 8 Mar 2022 14:41:19 +0100 Subject: [PATCH 1/3] fixed autocomplete issues with commands like "command subcommand subsubcommand ..." --- lib/LocalEchoController.js | 106 ++++++++++++++++++++----------------- lib/Utils.js | 31 +++++------ 2 files changed, 73 insertions(+), 64 deletions(-) diff --git a/lib/LocalEchoController.js b/lib/LocalEchoController.js index b2212a2..ff28413 100644 --- a/lib/LocalEchoController.js +++ b/lib/LocalEchoController.js @@ -1,14 +1,14 @@ -import { HistoryController } from "./HistoryController"; +import {HistoryController} from "./HistoryController"; import { closestLeftBoundary, closestRightBoundary, collectAutocompleteCandidates, countLines, getLastToken, + getSharedFragment, hasTailingWhitespace, isIncompleteInput, - offsetToColRow, - getSharedFragment + offsetToColRow } from "./Utils"; /** @@ -27,7 +27,7 @@ export default class LocalEchoController { this.term = term; this._handleTermData = this.handleTermData.bind(this); this._handleTermResize = this.handleTermResize.bind(this) - + this.history = new HistoryController(options.historySize || 10); this.maxAutocompleteEntries = options.maxAutocompleteEntries || 100; @@ -43,7 +43,7 @@ export default class LocalEchoController { }; this._disposables = []; - + if (term) { if (term.loadAddon) term.loadAddon(this); else this.attach(); @@ -55,6 +55,7 @@ export default class LocalEchoController { this.term = term; this.attach(); } + dispose() { this.detach(); } @@ -62,7 +63,7 @@ export default class LocalEchoController { ///////////////////////////////////////////////////////////////////////////// // User-Facing API ///////////////////////////////////////////////////////////////////////////// - + /** * Detach the controller from the terminal */ @@ -75,7 +76,7 @@ export default class LocalEchoController { this._disposables = []; } } - + /** * Attach controller to the terminal, handling events */ @@ -192,7 +193,7 @@ export default class LocalEchoController { // Compute item sizes and matrix row/cols const itemWidth = - items.reduce((width, item) => Math.max(width, item.length), 0) + padding; + items.reduce((width, item) => Math.max(width, item.length), 0) + padding; const wideCols = Math.floor(this._termSize.cols / itemWidth); const wideRows = Math.ceil(items.length / wideCols); @@ -223,7 +224,7 @@ export default class LocalEchoController { applyPrompts(input) { const prompt = (this._activePrompt || {}).prompt || ""; const continuationPrompt = - (this._activePrompt || {}).continuationPrompt || ""; + (this._activePrompt || {}).continuationPrompt || ""; return prompt + input.replace(/\n/g, "\n" + continuationPrompt); } @@ -251,10 +252,10 @@ export default class LocalEchoController { // Get the line we are currently in const promptCursor = this.applyPromptOffset(this._input, this._cursor); - const { col, row } = offsetToColRow( - currentPrompt, - promptCursor, - this._termSize.cols + const {col, row} = offsetToColRow( + currentPrompt, + promptCursor, + this._termSize.cols ); // First move on the last line @@ -288,10 +289,10 @@ export default class LocalEchoController { // Move the cursor to the appropriate row/col const newCursor = this.applyPromptOffset(newInput, this._cursor); const newLines = countLines(newPrompt, this._termSize.cols); - const { col, row } = offsetToColRow( - newPrompt, - newCursor, - this._termSize.cols + const {col, row} = offsetToColRow( + newPrompt, + newCursor, + this._termSize.cols ); const moveUpRows = newLines - row - 1; @@ -346,18 +347,18 @@ export default class LocalEchoController { // Estimate previous cursor position const prevPromptOffset = this.applyPromptOffset(this._input, this._cursor); - const { col: prevCol, row: prevRow } = offsetToColRow( - inputWithPrompt, - prevPromptOffset, - this._termSize.cols + const {col: prevCol, row: prevRow} = offsetToColRow( + inputWithPrompt, + prevPromptOffset, + this._termSize.cols ); // Estimate next cursor position const newPromptOffset = this.applyPromptOffset(this._input, newCursor); - const { col: newCol, row: newRow } = offsetToColRow( - inputWithPrompt, - newPromptOffset, - this._termSize.cols + const {col: newCol, row: newRow} = offsetToColRow( + inputWithPrompt, + newPromptOffset, + this._termSize.cols ); // Adjust vertically @@ -395,7 +396,7 @@ export default class LocalEchoController { * Erase a character at cursor location */ handleCursorErase(backspace) { - const { _cursor, _input } = this; + const {_cursor, _input} = this; if (backspace) { if (_cursor <= 0) return; const newInput = _input.substr(0, _cursor - 1) + _input.substr(_cursor); @@ -412,7 +413,7 @@ export default class LocalEchoController { * Insert character at cursor location */ handleCursorInsert(data) { - const { _cursor, _input } = this; + const {_cursor, _input} = this; const newInput = _input.substr(0, _cursor) + data + _input.substr(_cursor); this._cursor += data.length; this.setInput(newInput); @@ -441,9 +442,9 @@ export default class LocalEchoController { * input. This leads (most of the times) into a better formatted input. */ handleTermResize(data) { - const { rows, cols } = data; + const {rows, cols} = data; this.clearInput(); - this._termSize = { cols, rows }; + this._termSize = {cols, rows}; this.setInput(this._input, false); } @@ -534,7 +535,7 @@ export default class LocalEchoController { ofs = closestLeftBoundary(this._input, this._cursor); if (ofs != null) { this.setInput( - this._input.substr(0, ofs) + this._input.substr(this._cursor) + this._input.substr(0, ofs) + this._input.substr(this._cursor) ); this.setCursor(ofs); } @@ -561,8 +562,8 @@ export default class LocalEchoController { const inputFragment = this._input.substr(0, this._cursor); const hasTailingSpace = hasTailingWhitespace(inputFragment); const candidates = collectAutocompleteCandidates( - this._autocompleteHandlers, - inputFragment + this._autocompleteHandlers, + inputFragment ); // Sort candidates @@ -578,22 +579,29 @@ export default class LocalEchoController { } else if (candidates.length === 1) { // Just a single candidate? Complete const lastToken = getLastToken(inputFragment); - this.handleCursorInsert( - candidates[0].substr(lastToken.length) + " " - ); + if (hasTailingSpace) { + this.handleCursorInsert( + candidates[0] + " " + ); + } else { + this.handleCursorInsert( + candidates[0].substr(lastToken.length) + " " + ); + } + } else if (candidates.length <= this.maxAutocompleteEntries) { // search for a shared fragement const sameFragment = getSharedFragment(inputFragment, candidates); - + // if there's a shared fragement between the candidates // print complete the shared fragment - if (sameFragment) { - const lastToken = getLastToken(inputFragment); - this.handleCursorInsert( - sameFragment.substr(lastToken.length) - ); - } + // if (sameFragment) { + // const lastToken = getLastToken(inputFragment); + // this.handleCursorInsert( + // sameFragment.substr(lastToken.length) + // ); + // } // If we are less than maximum auto-complete candidates, print // them to the user and re-start prompt @@ -604,13 +612,13 @@ export default class LocalEchoController { // If we have more than maximum auto-complete candidates, print // them only if the user acknowledges a warning this.printAndRestartPrompt(() => - this.readChar( - `Display all ${candidates.length} possibilities? (y or n)` - ).then(yn => { - if (yn == "y" || yn == "Y") { - this.printWide(candidates); - } - }) + this.readChar( + `Display all ${candidates.length} possibilities? (y or n)` + ).then(yn => { + if (yn == "y" || yn == "Y") { + this.printWide(candidates); + } + }) ); } } else { diff --git a/lib/Utils.js b/lib/Utils.js index 6d4f103..40c1eb0 100644 --- a/lib/Utils.js +++ b/lib/Utils.js @@ -1,4 +1,4 @@ -import { parse } from "shell-quote"; +import {parse} from "shell-quote"; /** * Detects all the word boundaries on the given input @@ -25,10 +25,11 @@ export function wordBoundaries(input, leftSide = true) { */ export function closestLeftBoundary(input, offset) { const found = wordBoundaries(input, true) - .reverse() - .find(x => x < offset); + .reverse() + .find(x => x < offset); return found == null ? 0 : found; } + export function closestRightBoundary(input, offset) { const found = wordBoundaries(input, false).find(x => x > offset); return found == null ? input.length : found; @@ -42,7 +43,7 @@ export function closestRightBoundary(input, offset) { */ export function offsetToColRow(input, offset, maxCols) { let row = 0, - col = 0; + col = 0; for (let i = 0; i < offset; ++i) { const chr = input.charAt(i); @@ -58,7 +59,7 @@ export function offsetToColRow(input, offset, maxCols) { } } - return { row, col }; + return {row, col}; } /** @@ -94,10 +95,10 @@ export function isIncompleteInput(input) { } // Check for dangling boolean or pipe operations if ( - input - .split(/(\|\||\||&&)/g) - .pop() - .trim() == "" + input + .split(/(\|\||\||&&)/g) + .pop() + .trim() == "" ) { return true; } @@ -122,7 +123,7 @@ export function hasTailingWhitespace(input) { export function getLastToken(input) { // Empty expressions if (input.trim() === "") return ""; - if (hasTailingWhitespace(input)) return ""; + if (hasTailingWhitespace(input)) return input; // Last token const tokens = parse(input); @@ -148,7 +149,7 @@ export function collectAutocompleteCandidates(callbacks, input) { } // Collect all auto-complete candidates from the callbacks - const all = callbacks.reduce((candidates, { fn, args }) => { + const all = callbacks.reduce((candidates, {fn, args}) => { try { return candidates.concat(fn(index, tokens, ...args)); } catch (e) { @@ -166,14 +167,14 @@ export function getSharedFragment(fragment, candidates) { // end loop when fragment length = first candidate length if (fragment.length >= candidates[0].length) return fragment; - + // save old fragemnt const oldFragment = fragment; - + // get new fragment - fragment += candidates[0].slice(fragment.length, fragment.length+1); + fragment += candidates[0].slice(fragment.length, fragment.length + 1); - for (let i=0; i Date: Tue, 8 Mar 2022 14:50:51 +0100 Subject: [PATCH 2/3] removed unused code --- lib/LocalEchoController.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/lib/LocalEchoController.js b/lib/LocalEchoController.js index ff28413..38bfcc7 100644 --- a/lib/LocalEchoController.js +++ b/lib/LocalEchoController.js @@ -591,18 +591,6 @@ export default class LocalEchoController { } else if (candidates.length <= this.maxAutocompleteEntries) { - // search for a shared fragement - const sameFragment = getSharedFragment(inputFragment, candidates); - - // if there's a shared fragement between the candidates - // print complete the shared fragment - // if (sameFragment) { - // const lastToken = getLastToken(inputFragment); - // this.handleCursorInsert( - // sameFragment.substr(lastToken.length) - // ); - // } - // If we are less than maximum auto-complete candidates, print // them to the user and re-start prompt this.printAndRestartPrompt(() => { From 6b55a5356a2a5a2c0d2b812948c2dd6181735019 Mon Sep 17 00:00:00 2001 From: Stephan Kast Date: Thu, 10 Mar 2022 09:21:39 +0100 Subject: [PATCH 3/3] added "getInput()" --- lib/LocalEchoController.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/LocalEchoController.js b/lib/LocalEchoController.js index 38bfcc7..b30102e 100644 --- a/lib/LocalEchoController.js +++ b/lib/LocalEchoController.js @@ -304,6 +304,15 @@ export default class LocalEchoController { this._input = newInput; } + /** + * Get current input + * + * This function returns you the current input + */ + getInput(){ + return this._input; + } + /** * This function completes the current input, calls the given callback * and then re-displays the prompt.