Bug 1510422 - Fix autocomplete cache handling; r=Honza.

In Bug 1462394, we moved the autocomplete data handling
out of the JsTerm to the Redux store. In the process, we
regress some cases like `await n`, which should display
`navigator`, but isn't anymore when the user types the
whole sequence. Ctrl+Space would still show the popup,
which indicates that the issue is not on the server-side.

This issue is caused because our new code decides that
we should hit the cache when typing the `n`, and there's
nothing in the cache.

Previously, we were clearing the cache as soon as the input
last string wasn't alphanumeric, which we don't anymore.
To fix that, instead of relying on the last string of the
input (which could be wrong in cases like `x.["hello `), we
clear the cache when the autocomplete service returns a null
`matches` property.

In the JsPropertyProvider, we use to return null whenever
there isn't any search done (incorrect input, empty match prop, …).
So it seems like a good idea to bust the cache when the
server returns null.

This requires some changes to the autocomplete service, as well
as some in jsPropertyProvider (e.g. to handle `await `).

Tests are added both on the client and the frontend to make sure
we don't regress this (those tests fail without the actual fix).

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

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Nicolas Chevobbe 2018-11-29 13:58:57 +00:00
Родитель 4a4bcf8624
Коммит 047f13303a
6 изменённых файлов: 124 добавлений и 21 удалений

Просмотреть файл

@ -37,6 +37,10 @@ function autocomplete(state = getDefaultState(), action) {
return state;
}
if (action.data.matches === null) {
return getDefaultState();
}
return {
...state,
cache: {

Просмотреть файл

@ -188,6 +188,7 @@ skip-if = verify
[browser_jsterm_autocomplete_accept_no_scroll.js]
[browser_jsterm_autocomplete_array_no_index.js]
[browser_jsterm_autocomplete_arrow_keys.js]
[browser_jsterm_autocomplete_await.js]
[browser_jsterm_autocomplete_cached_results.js]
[browser_jsterm_autocomplete_commands.js]
[browser_jsterm_autocomplete_control_space.js]

Просмотреть файл

@ -0,0 +1,44 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// See Bug 585991.
const TEST_URI = `data:text/html;charset=utf-8,Autocomplete await expression`;
add_task(async function() {
// Run test with legacy JsTerm
await pushPref("devtools.webconsole.jsterm.codeMirror", false);
await performTests();
// And then run it with the CodeMirror-powered one.
await pushPref("devtools.webconsole.jsterm.codeMirror", true);
await performTests();
});
async function performTests() {
const { jsterm } = await openNewTabAndConsole(TEST_URI);
const { autocompletePopup } = jsterm;
info("Check that the await keyword is in the autocomplete");
await setInputValueForAutocompletion(jsterm, "aw");
checkJsTermCompletionValue(jsterm, " ait", "completeNode has expected value");
EventUtils.synthesizeKey("KEY_Tab");
is(jsterm.getInputValue(), "await", "'await' tab completion");
const updated = jsterm.once("autocomplete-updated");
EventUtils.sendString(" ");
await updated;
info("Check that the autocomplete popup is displayed");
const onPopUpOpen = autocompletePopup.once("popup-opened");
EventUtils.sendString("P");
await onPopUpOpen;
ok(autocompletePopup.isOpen, "popup is open");
ok(autocompletePopup.items.some(item => item.label === "Promise"),
"popup has expected `Promise` item");
}

Просмотреть файл

@ -1217,12 +1217,19 @@ WebConsoleActor.prototype =
invokeUnsafeGetter: false,
webconsoleActor: this,
selectedNodeActor: request.selectedNodeActor,
}) || {};
});
if (!hadDebuggee && dbgObject) {
this.dbg.removeDebuggee(this.evalWindow);
}
if (result === null) {
return {
from: this.actorID,
matches: null,
};
}
matches = result.matches || new Set();
matchProp = result.matchProp;
isElementAccess = result.isElementAccess;

Просмотреть файл

@ -113,11 +113,6 @@ function analyzeInputString(str) {
const nextNonSpaceCharIndex = after.indexOf(nextNonSpaceChar);
const previousNonSpaceChar = trimmedBefore[trimmedBefore.length - 1];
// There's only spaces after that, so we can return.
if (!nextNonSpaceChar) {
return buildReturnObject();
}
// If the previous char isn't a dot or opening bracket, and the next one isn't
// one either, and the current computed statement is not a
// variable/function/class declaration, update the start position.
@ -126,7 +121,16 @@ function analyzeInputString(str) {
previousNonSpaceChar !== "[" && nextNonSpaceChar !== "[" &&
!NO_AUTOCOMPLETE_PREFIXES.includes(currentLastStatement)
) {
start = i + nextNonSpaceCharIndex;
start = i + (
nextNonSpaceCharIndex >= 0
? nextNonSpaceCharIndex
: (after.length + 1)
);
}
// There's only spaces after that, so we can return.
if (!nextNonSpaceChar) {
return buildReturnObject();
}
// Let's jump to handle the next non-space char.

Просмотреть файл

@ -17,6 +17,8 @@
MAX_AUTOCOMPLETE_ATTEMPTS,
MAX_AUTOCOMPLETIONS
} = require("devtools/shared/webconsole/js-property-provider");
const RESERVED_JS_KEYWORDS = require("devtools/shared/webconsole/reserved-js-words");
addEventListener("load", startTest);
@ -106,6 +108,7 @@
doAutocompleteAfterOperator,
dontAutocompleteAfterDeclaration,
doKeywordsAutocomplete,
dontAutocomplete,
];
if (!isWorker) {
@ -166,7 +169,7 @@
info("test autocomplete for 'dump(window.foobarObject.)'");
let response = await client.autocomplete("dump(window.foobarObject.)");
ok(!response.matchProp, "matchProp");
is(response.matches.length, 0, "matches.length");
is(response.matches, null, "matches is null");
}
async function doAutocompleteLarge1(client) {
@ -419,23 +422,19 @@
info(`test autocomplete for '['`);
res = await client.autocomplete(`[`);
is(res.matches.length, 0, "it does not return anything");
is(res.isElementAccess, false);
is(res.matches, null, "it does not return anything");
info(`test autocomplete for '[1,2,3'`);
res = await client.autocomplete(`[1,2,3`);
is(res.matches.length, 0, "it does not return anything");
is(res.isElementAccess, false);
is(res.matches, null, "it does not return anything");
info(`test autocomplete for '["'`);
res = await client.autocomplete(`["`);
is(res.matches.length, 0, "it does not return anything");
is(res.isElementAccess, false);
is(res.matches, null, "it does not return anything");
info(`test autocomplete for '[;'`);
res = await client.autocomplete(`[;`);
is(res.matches.length, 0, "it does not return anything");
is(res.isElementAccess, false);
is(res.matches, null, "it does not return anything");
}
async function doAutocompleteCommands(client) {
@ -516,23 +515,23 @@
async function dontAutocompleteAfterDeclaration(client) {
info("test autocomplete for 'var win'");
let matches = (await client.autocomplete("var win")).matches;
is(matches.length, 0, "no autocompletion on a var declaration");
is(matches, null, "no autocompletion on a var declaration");
info("test autocomplete for 'const win'");
matches = (await client.autocomplete("const win")).matches;
is(matches.length, 0, "no autocompletion on a const declaration");
is(matches, null, "no autocompletion on a const declaration");
info("test autocomplete for 'let win'");
matches = (await client.autocomplete("let win")).matches;
is(matches.length, 0, "no autocompletion on a let declaration");
is(matches, null, "no autocompletion on a let declaration");
info("test autocomplete for 'function win'");
matches = (await client.autocomplete("function win")).matches;
is(matches.length, 0, "no autocompletion on a function declaration");
is(matches, null, "no autocompletion on a function declaration");
info("test autocomplete for 'class win'");
matches = (await client.autocomplete("class win")).matches;
is(matches.length, 0, "no autocompletion on a class declaration");
is(matches, null, "no autocompletion on a class declaration");
info("test autocomplete for 'const win = win'");
matches = (await client.autocomplete("const win = win")).matches;
@ -565,6 +564,50 @@ async function doKeywordsAutocomplete(client) {
"'function' is not returned when doing an element access");
}
async function dontAutocomplete(client) {
const inputs = [
"",
" ",
"\n",
"\n ",
" \n ",
" \n",
"true;",
"true,",
"({key:",
"a=",
"if(a<",
"if(a>",
"1+",
"1-",
"++",
"--",
"1*",
"2**",
"1/",
"1%",
"1|",
"1&",
"1^",
"~",
"1<<",
"1>>",
"1>>>",
"false||",
"false&&",
"x=true?",
"x=false?1:",
"!",
...RESERVED_JS_KEYWORDS.map(keyword => `${keyword} `),
...RESERVED_JS_KEYWORDS.map(keyword => `${keyword} `),
];
for (const input of inputs) {
info(`test autocomplete for "${input}"`);
let matches = (await client.autocomplete(input)).matches;
is(matches, null, `No autocomplete result for ${input}"`);
}
}
async function getAutocompleteMatches(client, input) {
info(`test autocomplete for "${input}"`);
const res = (await client.autocomplete(input));