Bug 1462394 - Handle autocompletion data fetching and caching in Redux; r=Honza.

This patch moves all the logic we currently have
baked-in JsTerm to handle the autocompletion data:
- deciding to fetch from the server or the cache
- handling concurrent requests
- managing the cache.

This is now done through dedicated Redux actions and reducers.
In the JsTerm, where the autocompletePopup still lives, we
handle those data changes in componentWillReceiveProps.

Some tests were modified in order to pass with these changes.

Differential Revision: https://phabricator.services.mozilla.com/D11454

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Nicolas Chevobbe 2018-11-12 16:07:37 +00:00
parent 918425999d
commit 7b12453efb
13 changed files with 342 additions and 147 deletions

View File

@ -0,0 +1,133 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {
AUTOCOMPLETE_CLEAR,
AUTOCOMPLETE_DATA_RECEIVE,
AUTOCOMPLETE_PENDING_REQUEST,
AUTOCOMPLETE_RETRIEVE_FROM_CACHE,
} = require("devtools/client/webconsole/constants");
/**
* Update the data used for the autocomplete popup in the console input (JsTerm).
*
* @param {Object} Object of the following shape:
* - {String} inputValue: the expression to complete.
* - {Int} cursor: The position of the cursor in the inputValue.
* - {WebConsoleClient} client: The webconsole client.
* - {String} frameActorId: The id of the frame we want to autocomplete in.
* - {Boolean} force: True to force a call to the server (as opposed to retrieve
* from the cache).
*/
function autocompleteUpdate({
inputValue,
cursor,
client,
frameActorId,
force,
}) {
return ({dispatch, getState}) => {
const {cache} = getState().autocomplete;
if (!force && (
!inputValue ||
/^[a-zA-Z0-9_$]/.test(inputValue.substring(cursor))
)) {
return dispatch(autocompleteClear());
}
const input = inputValue.substring(0, cursor);
const retrieveFromCache = !force &&
cache &&
cache.input &&
input.startsWith(cache.input) &&
/[a-zA-Z0-9]$/.test(input) &&
frameActorId === cache.frameActorId;
if (retrieveFromCache) {
return dispatch(autoCompleteDataRetrieveFromCache(input));
}
return dispatch(autocompleteDataFetch({input, frameActorId, client}));
};
}
/**
* Called when the autocompletion data should be cleared.
*/
function autocompleteClear() {
return {
type: AUTOCOMPLETE_CLEAR,
};
}
/**
* Called when the autocompletion data should be retrieved from the cache (i.e.
* client-side).
*
* @param {String} input: The input used to filter the cached data.
*/
function autoCompleteDataRetrieveFromCache(input) {
return {
type: AUTOCOMPLETE_RETRIEVE_FROM_CACHE,
input,
};
}
let currentRequestId = 0;
function generateRequestId() {
return currentRequestId++;
}
/**
* Action that fetch autocompletion data from the server.
*
* @param {Object} Object of the following shape:
* - {String} input: the expression that we want to complete.
* - {String} frameActorId: The id of the frame we want to autocomplete in.
* - {WebConsoleClient} client: The webconsole client.
*/
function autocompleteDataFetch({
input,
frameActorId,
client,
}) {
return ({dispatch}) => {
const id = generateRequestId();
dispatch({type: AUTOCOMPLETE_PENDING_REQUEST, id});
client.autocomplete(input, undefined, frameActorId).then(res => {
dispatch(autocompleteDataReceive(id, input, frameActorId, res));
}).catch(e => {
console.error("failed autocomplete", e);
dispatch(autocompleteClear());
});
};
}
/**
* Called when we receive the autocompletion data from the server.
*
* @param {Integer} id: The autocompletion request id. This will be used in the reducer to
* check that we update the state with the last request results.
* @param {String} input: The expression that was evaluated to get the data.
* - {String} frameActorId: The id of the frame the evaluation was made in.
* @param {Object} data: The actual data sent from the server.
*/
function autocompleteDataReceive(id, input, frameActorId, data) {
return {
type: AUTOCOMPLETE_DATA_RECEIVE,
id,
input,
frameActorId,
data,
};
}
module.exports = {
autocompleteUpdate,
autocompleteDataFetch,
autocompleteDataReceive,
};

View File

@ -7,6 +7,7 @@
"use strict"; "use strict";
const actionModules = [ const actionModules = [
require("./autocomplete"),
require("./filters"), require("./filters"),
require("./messages"), require("./messages"),
require("./ui"), require("./ui"),

View File

@ -4,6 +4,7 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/. # file, You can obtain one at http://mozilla.org/MPL/2.0/.
DevToolsModules( DevToolsModules(
'autocomplete.js',
'filters.js', 'filters.js',
'history.js', 'history.js',
'index.js', 'index.js',

View File

@ -39,7 +39,9 @@ const {
getHistory, getHistory,
getHistoryValue, getHistoryValue,
} = require("devtools/client/webconsole/selectors/history"); } = require("devtools/client/webconsole/selectors/history");
const {getAutocompleteState} = require("devtools/client/webconsole/selectors/autocomplete");
const historyActions = require("devtools/client/webconsole/actions/history"); const historyActions = require("devtools/client/webconsole/actions/history");
const autocompleteActions = require("devtools/client/webconsole/actions/autocomplete");
// Constants used for defining the direction of JSTerm input history navigation. // Constants used for defining the direction of JSTerm input history navigation.
const { const {
@ -77,6 +79,10 @@ class JSTerm extends Component {
codeMirrorEnabled: PropTypes.bool, codeMirrorEnabled: PropTypes.bool,
// Update position in the history after executing an expression (action). // Update position in the history after executing an expression (action).
updateHistoryPosition: PropTypes.func.isRequired, updateHistoryPosition: PropTypes.func.isRequired,
// Update autocomplete popup state.
autocompleteUpdate: PropTypes.func.isRequired,
// Data to be displayed in the autocomplete popup.
autocompleteData: PropTypes.object.isRequired,
}; };
} }
@ -94,40 +100,16 @@ class JSTerm extends Component {
this._inputEventHandler = this._inputEventHandler.bind(this); this._inputEventHandler = this._inputEventHandler.bind(this);
this._blurEventHandler = this._blurEventHandler.bind(this); this._blurEventHandler = this._blurEventHandler.bind(this);
this.onContextMenu = this.onContextMenu.bind(this); this.onContextMenu = this.onContextMenu.bind(this);
this.imperativeUpdate = this.imperativeUpdate.bind(this);
this.SELECTED_FRAME = -1; this.SELECTED_FRAME = -1;
/**
* Array that caches the user input suggestions received from the server.
* @private
* @type array
*/
this._autocompleteCache = null;
/**
* The input that caused the last request to the server, whose response is
* cached in the _autocompleteCache array.
* @private
* @type string
*/
this._autocompleteQuery = null;
/**
* The frameActorId used in the last autocomplete query. Whenever this changes
* the autocomplete cache must be invalidated.
* @private
* @type string
*/
this._lastFrameActorId = null;
/** /**
* Last input value. * Last input value.
* @type string * @type string
*/ */
this.lastInputValue = ""; this.lastInputValue = "";
this.currentAutoCompletionRequestId = null;
this.autocompletePopup = null; this.autocompletePopup = null;
this.inputNode = null; this.inputNode = null;
this.completeNode = null; this.completeNode = null;
@ -356,7 +338,7 @@ class JSTerm extends Component {
"Ctrl-Space": () => { "Ctrl-Space": () => {
if (!this.autocompletePopup.isOpen) { if (!this.autocompletePopup.isOpen) {
this.updateAutocompletion(true); this.fetchAutocompletionProperties(true);
return null; return null;
} }
@ -403,13 +385,32 @@ class JSTerm extends Component {
this.lastInputValue && this.setInputValue(this.lastInputValue); this.lastInputValue && this.setInputValue(this.lastInputValue);
} }
shouldComponentUpdate(nextProps, nextState) { componentWillReceiveProps(nextProps) {
this.imperativeUpdate(nextProps);
}
shouldComponentUpdate(nextProps) {
// XXX: For now, everything is handled in an imperative way and we // XXX: For now, everything is handled in an imperative way and we
// only want React to do the initial rendering of the component. // only want React to do the initial rendering of the component.
// This should be modified when the actual refactoring will take place. // This should be modified when the actual refactoring will take place.
return false; return false;
} }
/**
* Do all the imperative work needed after a Redux store update.
*
* @param {Object} nextProps: props passed from shouldComponentUpdate.
*/
imperativeUpdate(nextProps) {
if (
nextProps &&
nextProps.autocompleteData !== this.props.autocompleteData &&
nextProps.autocompleteData.pendingRequestId === null
) {
this.updateAutocompletionPopup(nextProps.autocompleteData);
}
}
/** /**
* Getter for the element that holds the messages we display. * Getter for the element that holds the messages we display.
* @type Element * @type Element
@ -773,7 +774,7 @@ class JSTerm extends Component {
const value = this.getInputValue(); const value = this.getInputValue();
if (this.lastInputValue !== value) { if (this.lastInputValue !== value) {
this.resizeInput(); this.resizeInput();
this.updateAutocompletion(); this.fetchAutocompletionProperties();
this.lastInputValue = value; this.lastInputValue = value;
} }
} }
@ -860,7 +861,7 @@ class JSTerm extends Component {
if (event.key === " " && !this.autocompletePopup.isOpen) { if (event.key === " " && !this.autocompletePopup.isOpen) {
// Open the autocompletion popup on Ctrl-Space (if it wasn't displayed). // Open the autocompletion popup on Ctrl-Space (if it wasn't displayed).
this.updateAutocompletion(true); this.fetchAutocompletionProperties(true);
event.preventDefault(); event.preventDefault();
} }
@ -1104,137 +1105,64 @@ class JSTerm extends Component {
} }
/** /**
* Retrieves properties maching the current input for the selected frame, either from
* the server or from a cache if possible.
* Will bail-out if there's some text selection in the input.
* *
* @param {Boolean} force: True to not perform any check before trying to show the * @param {Boolean} force: True to not perform any check before trying to show the
* autocompletion popup. Defaults to false. * autocompletion popup. Defaults to false.
* @fires autocomplete-updated
* @returns void
*/ */
async updateAutocompletion(force = false) { async fetchAutocompletionProperties(force = false) {
const inputValue = this.getInputValue(); const inputValue = this.getInputValue();
const {editor, inputNode} = this; const frameActorId = this.getFrameActor(this.SELECTED_FRAME);
const frameActor = this.getFrameActor(this.SELECTED_FRAME);
const cursor = this.getSelectionStart(); const cursor = this.getSelectionStart();
// Complete if: const {editor, inputNode} = this;
// - `force` is true OR if (
// - The input is not empty
// - AND there is not text selected
// - AND the input or frameActor are different from previous completion
// - AND there is not an alphanumeric (+ "_" and "$") right after the cursor
if (!force && (
!inputValue ||
(inputNode && inputNode.selectionStart != inputNode.selectionEnd) || (inputNode && inputNode.selectionStart != inputNode.selectionEnd) ||
(editor && editor.getSelection()) || (editor && editor.getSelection())
( ) {
!force &&
this.lastInputValue === inputValue &&
frameActor === this._lastFrameActorId
) ||
/^[a-zA-Z0-9_$]/.test(inputValue.substring(cursor))
)) {
this.clearCompletion(); this.clearCompletion();
this.emit("autocomplete-updated"); this.emit("autocomplete-updated");
return; return;
} }
const input = this.getInputValueBeforeCursor(); this.props.autocompleteUpdate({
inputValue,
// If the current input starts with the previous input, then we already cursor,
// have a list of suggestions and we just need to filter the cached frameActorId,
// suggestions. When the current input ends with a non-alphanumeric character we ask force,
// the server again for suggestions. client: this.webConsoleClient,
});
// Check if last character is non-alphanumeric
if (!/[a-zA-Z0-9]$/.test(input) || frameActor != this._lastFrameActorId) {
this._autocompleteQuery = null;
this._autocompleteCache = null;
}
if (this._autocompleteQuery && input.startsWith(this._autocompleteQuery)) {
let filterBy = input;
if (this._autocompleteCache.isElementAccess) {
// if we're performing an element access, we can simply retrieve whatever comes
// after the last opening bracket.
filterBy = input.substring(input.lastIndexOf("[") + 1);
} else {
// Find the last non-alphanumeric other than "_", ":", or "$" if it exists.
const lastNonAlpha = input.match(/[^a-zA-Z0-9_$:][a-zA-Z0-9_$:]*$/);
// If input contains non-alphanumerics, use the part after the last one
// to filter the cache.
if (lastNonAlpha) {
filterBy = input.substring(input.lastIndexOf(lastNonAlpha) + 1);
}
}
const stripWrappingQuotes = s => s.replace(/^['"`](.+(?=['"`]$))['"`]$/g, "$1");
const filterByLc = filterBy.toLocaleLowerCase();
const looseMatching = !filterBy || filterBy[0].toLocaleLowerCase() === filterBy[0];
const needStripQuote = this._autocompleteCache.isElementAccess
&& !/^[`"']/.test(filterBy);
const newList = this._autocompleteCache.matches.filter(l => {
if (needStripQuote) {
l = stripWrappingQuotes(l);
}
if (looseMatching) {
return l.toLocaleLowerCase().startsWith(filterByLc);
}
return l.startsWith(filterBy);
});
this._receiveAutocompleteProperties(null, {
matches: newList,
matchProp: filterBy,
isElementAccess: this._autocompleteCache.isElementAccess,
});
return;
}
const requestId = gSequenceId();
this._lastFrameActorId = frameActor;
this.currentAutoCompletionRequestId = requestId;
const message = await this.webConsoleClient.autocomplete(input, cursor, frameActor);
this._receiveAutocompleteProperties(requestId, message);
} }
/** /**
* Handler for the autocompletion results. This method takes * Takes the data returned by the server and update the autocomplete popup state (i.e.
* the completion result received from the server and updates the UI * its visibility and items).
* accordingly.
* *
* @param number requestId * @param {Object} data
* Request ID. * The autocompletion data as returned by the webconsole actor's autocomplete
* @param object message * service. Should be of the following shape:
* The JSON message which holds the completion results received from * {
* the content process. * matches: {Array} array of the properties matching the input,
* matchProp: {String} The string used to filter the properties,
* isElementAccess: {Boolean} True when the input is an element access,
* i.e. `document["addEve`.
* }
* @fires autocomplete-updated
*/ */
_receiveAutocompleteProperties(requestId, message) { updateAutocompletionPopup(data) {
if (this.currentAutoCompletionRequestId !== requestId) { const {matches, matchProp, isElementAccess} = data;
return;
}
this.currentAutoCompletionRequestId = null;
// Cache whatever came from the server if the last char is
// alphanumeric, '.' or '['.
const inputUntilCursor = this.getInputValueBeforeCursor();
if (requestId != null && /[a-zA-Z0-9.\[]$/.test(inputUntilCursor)) {
this._autocompleteCache = {
matches: message.matches,
matchProp: message.matchProp,
isElementAccess: message.isElementAccess,
};
this._autocompleteQuery = inputUntilCursor;
}
const {matches, matchProp, isElementAccess} = message;
if (!matches.length) { if (!matches.length) {
this.clearCompletion(); this.clearCompletion();
this.emit("autocomplete-updated"); this.emit("autocomplete-updated");
return; return;
} }
const inputUntilCursor = this.getInputValueBeforeCursor();
const items = matches.map(label => { const items = matches.map(label => {
let preLabel = label.substring(0, matchProp.length); let preLabel = label.substring(0, matchProp.length);
// If the user is performing an element access, and if they did not typed a quote, // If the user is performing an element access, and if they did not typed a quote,
@ -1673,15 +1601,27 @@ function mapStateToProps(state) {
return { return {
history: getHistory(state), history: getHistory(state),
getValueFromHistory: (direction) => getHistoryValue(state, direction), getValueFromHistory: (direction) => getHistoryValue(state, direction),
autocompleteData: getAutocompleteState(state),
}; };
} }
function mapDispatchToProps(dispatch) { function mapDispatchToProps(dispatch) {
return { return {
appendToHistory: (expr) => dispatch(historyActions.appendToHistory(expr)), appendToHistory: (expr) => dispatch(historyActions.appendToHistory(expr)),
clearHistory: () => dispatch(historyActions.clearHistory()), clearHistory: () => dispatch(historyActions.clearHistory()),
updateHistoryPosition: (direction, expression) => updateHistoryPosition: (direction, expression) =>
dispatch(historyActions.updateHistoryPosition(direction, expression)), dispatch(historyActions.updateHistoryPosition(direction, expression)),
autocompleteUpdate: ({inputValue, cursor, frameActorId, force, client}) => dispatch(
autocompleteActions.autocompleteUpdate({
inputValue,
cursor,
frameActorId,
force,
client,
})
),
autocompleteBailOut: () => dispatch(autocompleteActions.autocompleteBailOut()),
}; };
} }

View File

@ -8,6 +8,10 @@
const actionTypes = { const actionTypes = {
APPEND_NOTIFICATION: "APPEND_NOTIFICATION", APPEND_NOTIFICATION: "APPEND_NOTIFICATION",
APPEND_TO_HISTORY: "APPEND_TO_HISTORY", APPEND_TO_HISTORY: "APPEND_TO_HISTORY",
AUTOCOMPLETE_CLEAR: "AUTOCOMPLETE_CLEAR",
AUTOCOMPLETE_DATA_RECEIVE: "AUTOCOMPLETE_DATA_RECEIVE",
AUTOCOMPLETE_PENDING_REQUEST: "AUTOCOMPLETE_PENDING_REQUEST",
AUTOCOMPLETE_RETRIEVE_FROM_CACHE: "AUTOCOMPLETE_RETRIEVE_FROM_CACHE",
BATCH_ACTIONS: "BATCH_ACTIONS", BATCH_ACTIONS: "BATCH_ACTIONS",
CLEAR_HISTORY: "CLEAR_HISTORY", CLEAR_HISTORY: "CLEAR_HISTORY",
DEFAULT_FILTERS_RESET: "DEFAULT_FILTERS_RESET", DEFAULT_FILTERS_RESET: "DEFAULT_FILTERS_RESET",

View File

@ -0,0 +1,104 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {
AUTOCOMPLETE_CLEAR,
AUTOCOMPLETE_DATA_RECEIVE,
AUTOCOMPLETE_PENDING_REQUEST,
AUTOCOMPLETE_RETRIEVE_FROM_CACHE,
} = require("devtools/client/webconsole/constants");
function getDefaultState() {
return Object.freeze({
cache: null,
matches: [],
matchProp: null,
isElementAccess: false,
pendingRequestId: null,
});
}
function autocomplete(state = getDefaultState(), action) {
switch (action.type) {
case AUTOCOMPLETE_CLEAR:
return getDefaultState();
case AUTOCOMPLETE_RETRIEVE_FROM_CACHE:
return autoCompleteRetrieveFromCache(state, action);
case AUTOCOMPLETE_PENDING_REQUEST:
return {
...state,
cache: null,
pendingRequestId: action.id,
};
case AUTOCOMPLETE_DATA_RECEIVE:
if (action.id !== state.pendingRequestId) {
return state;
}
return {
...state,
cache: {
input: action.input,
frameActorId: action.frameActorId,
...action.data,
},
pendingRequestId: null,
...action.data,
};
}
return state;
}
/**
* Retrieve from cache action reducer.
*
* @param {Object} state
* @param {Object} action
* @returns {Object} new state.
*/
function autoCompleteRetrieveFromCache(state, action) {
const {input} = action;
const {cache} = state;
let filterBy = input;
if (cache.isElementAccess) {
// if we're performing an element access, we can simply retrieve whatever comes
// after the last opening bracket.
filterBy = input.substring(input.lastIndexOf("[") + 1);
} else {
// Find the last non-alphanumeric other than "_", ":", or "$" if it exists.
const lastNonAlpha = input.match(/[^a-zA-Z0-9_$:][a-zA-Z0-9_$:]*$/);
// If input contains non-alphanumerics, use the part after the last one
// to filter the cache.
if (lastNonAlpha) {
filterBy = input.substring(input.lastIndexOf(lastNonAlpha) + 1);
}
}
const stripWrappingQuotes = s => s.replace(/^['"`](.+(?=['"`]$))['"`]$/g, "$1");
const filterByLc = filterBy.toLocaleLowerCase();
const looseMatching = !filterBy || filterBy[0].toLocaleLowerCase() === filterBy[0];
const needStripQuote = cache.isElementAccess && !/^[`"']/.test(filterBy);
const newList = cache.matches.filter(l => {
if (needStripQuote) {
l = stripWrappingQuotes(l);
}
if (looseMatching) {
return l.toLocaleLowerCase().startsWith(filterByLc);
}
return l.startsWith(filterBy);
});
return {
...state,
matches: newList,
matchProp: filterBy,
isElementAccess: cache.isElementAccess,
};
}
exports.autocomplete = autocomplete;

View File

@ -5,6 +5,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict"; "use strict";
const { autocomplete } = require("./autocomplete");
const { filters } = require("./filters"); const { filters } = require("./filters");
const { messages } = require("./messages"); const { messages } = require("./messages");
const { prefs } = require("./prefs"); const { prefs } = require("./prefs");
@ -15,6 +16,7 @@ const { history } = require("./history");
const { objectInspector } = require("devtools/client/shared/components/reps/reps.js"); const { objectInspector } = require("devtools/client/shared/components/reps/reps.js");
exports.reducers = { exports.reducers = {
autocomplete,
filters, filters,
messages, messages,
prefs, prefs,

View File

@ -4,6 +4,7 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/. # file, You can obtain one at http://mozilla.org/MPL/2.0/.
DevToolsModules( DevToolsModules(
'autocomplete.js',
'filters.js', 'filters.js',
'history.js', 'history.js',
'index.js', 'index.js',

View File

@ -0,0 +1,13 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
function getAutocompleteState(state) {
return state.autocomplete;
}
module.exports = {
getAutocompleteState,
};

View File

@ -4,6 +4,7 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/. # file, You can obtain one at http://mozilla.org/MPL/2.0/.
DevToolsModules( DevToolsModules(
'autocomplete.js',
'filters.js', 'filters.js',
'history.js', 'history.js',
'messages.js', 'messages.js',

View File

@ -20,7 +20,7 @@ add_task(async function() {
}); });
async function performTests() { async function performTests() {
const { jsterm } = await openNewTabAndConsole(TEST_URI); const { jsterm, ui } = await openNewTabAndConsole(TEST_URI);
const { autocompletePopup: popup } = jsterm; const { autocompletePopup: popup } = jsterm;
@ -33,7 +33,7 @@ async function performTests() {
await onPopupOpen; await onPopupOpen;
ok(popup.isOpen, "popup is open"); ok(popup.isOpen, "popup is open");
const cacheMatches = jsterm._autocompleteCache.matches; const cacheMatches = ui.consoleOutput.getStore().getState().autocomplete.cache.matches;
is(popup.itemCount, cacheMatches.length, "popup.itemCount is correct"); is(popup.itemCount, cacheMatches.length, "popup.itemCount is correct");
ok(cacheMatches.includes("addEventListener"), ok(cacheMatches.includes("addEventListener"),
"addEventListener is in the list of suggestions"); "addEventListener is in the list of suggestions");

View File

@ -69,11 +69,8 @@ function hideAutocompletePopup(jsterm) {
function setJsTermValueForCompletion(jsterm, value) { function setJsTermValueForCompletion(jsterm, value) {
// setInputValue does not trigger the autocompletion; // setInputValue does not trigger the autocompletion;
// we need to call `updateAutocompletion` in order to display the popup. And since // we need to call `fetchAutocompletionProperties` in order to display the popup.
// setInputValue sets lastInputValue and updateAutocompletion checks it to trigger
// the autocompletion request, we reset it.
jsterm.setInputValue(value); jsterm.setInputValue(value);
jsterm.lastInputValue = null; jsterm.fetchAutocompletionProperties();
jsterm.updateAutocompletion();
} }

View File

@ -43,10 +43,8 @@ module.exports = async function() {
for (const char of Array.from(input)) { for (const char of Array.from(input)) {
const onPopupOpened = jsterm.autocompletePopup.once("popup-opened"); const onPopupOpened = jsterm.autocompletePopup.once("popup-opened");
jsterm.insertStringAtCursor(char); jsterm.insertStringAtCursor(char);
// We need to remove the lastInputValue set by setInputValue(called by // We need to trigger autocompletion update to show the popup.
// insertStringAtCursor), and trigger autocompletion update to show the popup. jsterm.fetchAutocompletionProperties();
jsterm.lastInputValue = null;
jsterm.updateAutocompletion();
await onPopupOpened; await onPopupOpened;
} }