Skip to content

fix autocomplete issues with "command subcommand" #57

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 57 additions & 52 deletions lib/LocalEchoController.js
Original file line number Diff line number Diff line change
@@ -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";

/**
Expand All @@ -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;

Expand All @@ -43,7 +43,7 @@ export default class LocalEchoController {
};

this._disposables = [];

if (term) {
if (term.loadAddon) term.loadAddon(this);
else this.attach();
Expand All @@ -55,14 +55,15 @@ export default class LocalEchoController {
this.term = term;
this.attach();
}

dispose() {
this.detach();
}

/////////////////////////////////////////////////////////////////////////////
// User-Facing API
/////////////////////////////////////////////////////////////////////////////

/**
* Detach the controller from the terminal
*/
Expand All @@ -75,7 +76,7 @@ export default class LocalEchoController {
this._disposables = [];
}
}

/**
* Attach controller to the terminal, handling events
*/
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;

Expand All @@ -303,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.
Expand Down Expand Up @@ -346,18 +356,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
Expand Down Expand Up @@ -395,7 +405,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);
Expand All @@ -412,7 +422,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);
Expand Down Expand Up @@ -441,9 +451,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);
}

Expand Down Expand Up @@ -534,7 +544,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);
}
Expand All @@ -561,8 +571,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
Expand All @@ -578,23 +588,18 @@ 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) + " "
);
} 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);
if (hasTailingSpace) {
this.handleCursorInsert(
sameFragment.substr(lastToken.length)
candidates[0] + " "
);
} else {
this.handleCursorInsert(
candidates[0].substr(lastToken.length) + " "
);
}

} else if (candidates.length <= this.maxAutocompleteEntries) {

// If we are less than maximum auto-complete candidates, print
// them to the user and re-start prompt
this.printAndRestartPrompt(() => {
Expand All @@ -604,13 +609,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 {
Expand Down
31 changes: 16 additions & 15 deletions lib/Utils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { parse } from "shell-quote";
import {parse} from "shell-quote";

/**
* Detects all the word boundaries on the given input
Expand All @@ -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;
Expand All @@ -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);
Expand All @@ -58,7 +59,7 @@ export function offsetToColRow(input, offset, maxCols) {
}
}

return { row, col };
return {row, col};
}

/**
Expand Down Expand Up @@ -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;
}
Expand All @@ -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);
Expand All @@ -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) {
Expand All @@ -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<candidates.length; i++ ) {
for (let i = 0; i < candidates.length; i++) {

// return null when there's a wrong candidate
if (!candidates[i].startsWith(oldFragment)) return null;
Expand Down