зеркало из https://github.com/mozilla/pjs.git
bug 568649 - JSTerm keybindings - tab completion. r=dietrich
This commit is contained in:
Родитель
225affa097
Коммит
ec464caf84
|
@ -2091,6 +2091,220 @@ function NodeFactory(aFactoryType, aNameSpace, aDocument)
|
|||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// JS Completer
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
const STATE_NORMAL = 0;
|
||||
const STATE_QUOTE = 2;
|
||||
const STATE_DQUOTE = 3;
|
||||
|
||||
const OPEN_BODY = '{[('.split('');
|
||||
const CLOSE_BODY = '}])'.split('');
|
||||
const OPEN_CLOSE_BODY = {
|
||||
'{': '}',
|
||||
'[': ']',
|
||||
'(': ')'
|
||||
};
|
||||
|
||||
/**
|
||||
* Analyses a given string to find the last statement that is interesting for
|
||||
* later completion.
|
||||
*
|
||||
* @param string aStr
|
||||
* A string to analyse.
|
||||
*
|
||||
* @returns object
|
||||
* If there was an error in the string detected, then a object like
|
||||
*
|
||||
* { err: "ErrorMesssage" }
|
||||
*
|
||||
* is returned, otherwise a object like
|
||||
*
|
||||
* {
|
||||
* state: STATE_NORMAL|STATE_QUOTE|STATE_DQUOTE,
|
||||
* startPos: index of where the last statement begins
|
||||
* }
|
||||
*/
|
||||
function findCompletionBeginning(aStr)
|
||||
{
|
||||
let bodyStack = [];
|
||||
|
||||
let state = STATE_NORMAL;
|
||||
let start = 0;
|
||||
let c;
|
||||
for (let i = 0; i < aStr.length; i++) {
|
||||
c = aStr[i];
|
||||
|
||||
switch (state) {
|
||||
// Normal JS state.
|
||||
case STATE_NORMAL:
|
||||
if (c == '"') {
|
||||
state = STATE_DQUOTE;
|
||||
}
|
||||
else if (c == '\'') {
|
||||
state = STATE_QUOTE;
|
||||
}
|
||||
else if (c == ';') {
|
||||
start = i + 1;
|
||||
}
|
||||
else if (c == ' ') {
|
||||
start = i + 1;
|
||||
}
|
||||
else if (OPEN_BODY.indexOf(c) != -1) {
|
||||
bodyStack.push({
|
||||
token: c,
|
||||
start: start
|
||||
});
|
||||
start = i + 1;
|
||||
}
|
||||
else if (CLOSE_BODY.indexOf(c) != -1) {
|
||||
var last = bodyStack.pop();
|
||||
if (OPEN_CLOSE_BODY[last.token] != c) {
|
||||
return {
|
||||
err: "syntax error"
|
||||
};
|
||||
}
|
||||
if (c == '}') {
|
||||
start = i + 1;
|
||||
}
|
||||
else {
|
||||
start = last.start;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
// Double quote state > " <
|
||||
case STATE_DQUOTE:
|
||||
if (c == '\\') {
|
||||
i ++;
|
||||
}
|
||||
else if (c == '\n') {
|
||||
return {
|
||||
err: "unterminated string literal"
|
||||
};
|
||||
}
|
||||
else if (c == '"') {
|
||||
state = STATE_NORMAL;
|
||||
}
|
||||
break;
|
||||
|
||||
// Single quoate state > ' <
|
||||
case STATE_QUOTE:
|
||||
if (c == '\\') {
|
||||
i ++;
|
||||
}
|
||||
else if (c == '\n') {
|
||||
return {
|
||||
err: "unterminated string literal"
|
||||
};
|
||||
return;
|
||||
}
|
||||
else if (c == '\'') {
|
||||
state = STATE_NORMAL;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
state: state,
|
||||
startPos: start
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a list of properties, that are possible matches based on the passed
|
||||
* scope and inputValue.
|
||||
*
|
||||
* @param object aScope
|
||||
* Scope to use for the completion.
|
||||
*
|
||||
* @param string aInputValue
|
||||
* Value that should be completed.
|
||||
*
|
||||
* @returns null or object
|
||||
* If no completion valued could be computed, null is returned,
|
||||
* otherwise a object with the following form is returned:
|
||||
* {
|
||||
* matches: [ string, string, string ],
|
||||
* matchProp: Last part of the inputValue that was used to find
|
||||
* the matches-strings.
|
||||
* }
|
||||
*/
|
||||
function JSPropertyProvider(aScope, aInputValue)
|
||||
{
|
||||
let obj = aScope;
|
||||
|
||||
// Analyse the aInputValue and find the beginning of the last part that
|
||||
// should be completed.
|
||||
let beginning = findCompletionBeginning(aInputValue);
|
||||
|
||||
// There was an error analysing the string.
|
||||
if (beginning.err) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If the current state is not STATE_NORMAL, then we are inside of an string
|
||||
// which means that no completion is possible.
|
||||
if (beginning.state != STATE_NORMAL) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let completionPart = aInputValue.substring(beginning.startPos);
|
||||
|
||||
// Don't complete on just an empty string.
|
||||
if (completionPart.trim() == "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
let properties = completionPart.split('.');
|
||||
let matchProp;
|
||||
if (properties.length > 1) {
|
||||
matchProp = properties[properties.length - 1].trimLeft();
|
||||
properties.pop();
|
||||
for each (var prop in properties) {
|
||||
prop = prop.trim();
|
||||
|
||||
// If obj is undefined or null, then there is no change to run
|
||||
// completion on it. Exit here.
|
||||
if (typeof obj === "undefined" || obj === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if prop is a getter function on obj. Functions can change other
|
||||
// stuff so we can't execute them to get the next object. Stop here.
|
||||
if (obj.__lookupGetter__(prop)) {
|
||||
return null;
|
||||
}
|
||||
obj = obj[prop];
|
||||
}
|
||||
}
|
||||
else {
|
||||
matchProp = properties[0].trimLeft();
|
||||
}
|
||||
|
||||
// If obj is undefined or null, then there is no change to run
|
||||
// completion on it. Exit here.
|
||||
if (typeof obj === "undefined" || obj === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let matches = [];
|
||||
for (var prop in obj) {
|
||||
matches.push(prop);
|
||||
}
|
||||
|
||||
matches = matches.filter(function(item) {
|
||||
return item.indexOf(matchProp) == 0;
|
||||
}).sort();
|
||||
|
||||
return {
|
||||
matchProp: matchProp,
|
||||
matches: matches
|
||||
};
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// JSTerm
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
@ -2140,6 +2354,13 @@ function JSTerm(aContext, aParentNode, aMixin)
|
|||
}
|
||||
|
||||
JSTerm.prototype = {
|
||||
|
||||
propertyProvider: JSPropertyProvider,
|
||||
|
||||
COMPLETE_FORWARD: 0,
|
||||
COMPLETE_BACKWARD: 1,
|
||||
COMPLETE_HINT_ONLY: 2,
|
||||
|
||||
init: function JST_init()
|
||||
{
|
||||
this.createSandbox();
|
||||
|
@ -2262,11 +2483,11 @@ JSTerm.prototype = {
|
|||
case 97:
|
||||
// control-a
|
||||
tmp = self.codeInputString;
|
||||
setTimeout(function() {
|
||||
self.inputNode.value = tmp;
|
||||
self.inputNode.setSelectionRange(0, 0);
|
||||
},0);
|
||||
break;
|
||||
setTimeout(function() {
|
||||
self.inputNode.value = tmp;
|
||||
self.inputNode.setSelectionRange(0, 0);
|
||||
}, 0);
|
||||
break;
|
||||
case 101:
|
||||
// control-e
|
||||
tmp = self.codeInputString;
|
||||
|
@ -2274,7 +2495,7 @@ JSTerm.prototype = {
|
|||
setTimeout(function(){
|
||||
var endPos = tmp.length + 1;
|
||||
self.inputNode.value = tmp;
|
||||
},0);
|
||||
}, 0);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
|
@ -2316,8 +2537,15 @@ JSTerm.prototype = {
|
|||
break;
|
||||
case 9:
|
||||
// tab key
|
||||
// TODO: this.tabComplete();
|
||||
// see bug 568649
|
||||
// If there are more than one possible completion, pressing tab
|
||||
// means taking the next completion, shift_tab means taking
|
||||
// the previous completion.
|
||||
if (aEvent.shiftKey) {
|
||||
self.complete(self.COMPLETE_BACKWARD);
|
||||
}
|
||||
else {
|
||||
self.complete(self.COMPLETE_FORWARD);
|
||||
}
|
||||
var bool = aEvent.cancelable;
|
||||
if (bool) {
|
||||
aEvent.preventDefault();
|
||||
|
@ -2327,7 +2555,24 @@ JSTerm.prototype = {
|
|||
}
|
||||
aEvent.target.focus();
|
||||
break;
|
||||
case 8:
|
||||
// backspace key
|
||||
case 46:
|
||||
// delete key
|
||||
// necessary so that default is not reached.
|
||||
break;
|
||||
default:
|
||||
// all not handled keys
|
||||
// Store the current inputNode value. If the value is the same
|
||||
// after keyDown event was handled (after 0ms) then the user
|
||||
// moved the cursor. If the value changed, then call the complete
|
||||
// function to show completion on new value.
|
||||
var value = self.inputNode.value;
|
||||
setTimeout(function() {
|
||||
if (self.inputNode.value !== value) {
|
||||
self.complete(self.COMPLETE_HINT_ONLY);
|
||||
}
|
||||
}, 0);
|
||||
break;
|
||||
}
|
||||
return;
|
||||
|
@ -2392,9 +2637,120 @@ JSTerm.prototype = {
|
|||
|
||||
history: [],
|
||||
|
||||
tabComplete: function JSTF_tabComplete(aInputValue) {
|
||||
// parse input value:
|
||||
// TODO: see bug 568649
|
||||
// Stores the data for the last completion.
|
||||
lastCompletion: null,
|
||||
|
||||
/**
|
||||
* Completes the current typed text in the inputNode. Completion is performed
|
||||
* only if the selection/cursor is at the end of the string. If no completion
|
||||
* is found, the current inputNode value and cursor/selection stay.
|
||||
*
|
||||
* @param int type possible values are
|
||||
* - this.COMPLETE_FORWARD: If there is more than one possible completion
|
||||
* and the input value stayed the same compared to the last time this
|
||||
* function was called, then the next completion of all possible
|
||||
* completions is used. If the value changed, then the first possible
|
||||
* completion is used and the selection is set from the current
|
||||
* cursor position to the end of the completed text.
|
||||
* If there is only one possible completion, then this completion
|
||||
* value is used and the cursor is put at the end of the completion.
|
||||
* - this.COMPLETE_BACKWARD: Same as this.COMPLETE_FORWARD but if the
|
||||
* value stayed the same as the last time the function was called,
|
||||
* then the previous completion of all possible completions is used.
|
||||
* - this.COMPLETE_HINT_ONLY: If there is more than one possible
|
||||
* completion and the input value stayed the same compared to the
|
||||
* last time this function was called, then the same completion is
|
||||
* used again. If there is only one possible completion, then
|
||||
* the inputNode.value is set to this value and the selection is set
|
||||
* from the current cursor position to the end of the completed text.
|
||||
*
|
||||
* @returns void
|
||||
*/
|
||||
complete: function JSTF_complete(type)
|
||||
{
|
||||
let inputNode = this.inputNode;
|
||||
let inputValue = inputNode.value;
|
||||
let selStart = inputNode.selectionStart, selEnd = inputNode.selectionEnd;
|
||||
|
||||
// 'Normalize' the selection so that end is always after start.
|
||||
if (selStart > selEnd) {
|
||||
let newSelEnd = selStart;
|
||||
selStart = selEnd;
|
||||
selEnd = newSelEnd;
|
||||
}
|
||||
|
||||
// Only complete if the selection is at the end of the input.
|
||||
if (selEnd != inputValue.length) {
|
||||
this.lastCompletion = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the selected text from the inputValue.
|
||||
inputValue = inputValue.substring(0, selStart);
|
||||
|
||||
let matches;
|
||||
let matchIndexToUse;
|
||||
let matchOffset;
|
||||
let completionStr;
|
||||
|
||||
// If there is a saved completion from last time and the used value for
|
||||
// completion stayed the same, then use the stored completion.
|
||||
if (this.lastCompletion && inputValue == this.lastCompletion.value) {
|
||||
matches = this.lastCompletion.matches;
|
||||
matchOffset = this.lastCompletion.matchOffset;
|
||||
if (type === this.COMPLETE_BACKWARD) {
|
||||
this.lastCompletion.index --;
|
||||
}
|
||||
else if (type === this.COMPLETE_FORWARD) {
|
||||
this.lastCompletion.index ++;
|
||||
}
|
||||
matchIndexToUse = this.lastCompletion.index;
|
||||
}
|
||||
else {
|
||||
// Look up possible completion values.
|
||||
let completion = this.propertyProvider(this.sandbox.window, inputValue);
|
||||
if (!completion) {
|
||||
return;
|
||||
}
|
||||
matches = completion.matches;
|
||||
matchIndexToUse = 0;
|
||||
matchOffset = completion.matchProp.length
|
||||
// Store this match;
|
||||
this.lastCompletion = {
|
||||
index: 0,
|
||||
value: inputValue,
|
||||
matches: matches,
|
||||
matchOffset: matchOffset
|
||||
};
|
||||
}
|
||||
|
||||
if (matches.length != 0) {
|
||||
// Ensure that the matchIndexToUse is always a valid array index.
|
||||
if (matchIndexToUse < 0) {
|
||||
matchIndexToUse = matches.length + (matchIndexToUse % matches.length);
|
||||
if (matchIndexToUse == matches.length) {
|
||||
matchIndexToUse = 0;
|
||||
}
|
||||
}
|
||||
else {
|
||||
matchIndexToUse = matchIndexToUse % matches.length;
|
||||
}
|
||||
|
||||
completionStr = matches[matchIndexToUse].substring(matchOffset);
|
||||
this.inputNode.value = inputValue + completionStr;
|
||||
|
||||
selEnd = inputValue.length + completionStr.length;
|
||||
|
||||
// If there is more than one possible completion or the completed part
|
||||
// should get displayed only without moving the cursor at the end of the
|
||||
// completion.
|
||||
if (matches.length > 1 || type === this.COMPLETE_HINT_ONLY) {
|
||||
inputNode.setSelectionRange(selStart, selEnd);
|
||||
}
|
||||
else {
|
||||
inputNode.setSelectionRange(selEnd, selEnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -55,6 +55,7 @@ _BROWSER_TEST_PAGES = \
|
|||
test-filter.html \
|
||||
test-observe-http-ajax.html \
|
||||
test-data.json \
|
||||
test-property-provider.html \
|
||||
$(NULL)
|
||||
|
||||
libs:: $(_BROWSER_TEST_FILES)
|
||||
|
|
|
@ -63,6 +63,8 @@ const TEST_NETWORK_URI = "http://example.com/browser/toolkit/components/console/
|
|||
|
||||
const TEST_FILTER_URI = "http://example.com/browser/toolkit/components/console/hudservice/tests/browser/test-filter.html";
|
||||
|
||||
const TEST_PROPERTY_PROVIDER_URI = "http://example.com/browser/toolkit/components/console/hudservice/tests/browser/test-property-provider.html";
|
||||
|
||||
function noCacheUriSpec(aUriSpec) {
|
||||
return aUriSpec + "?_=" + Date.now();
|
||||
}
|
||||
|
@ -430,6 +432,77 @@ function testConsoleHistory()
|
|||
is (input.value, executeList[idxLast], "check history next idx:" + idxLast);
|
||||
}
|
||||
|
||||
// test property provider
|
||||
function testPropertyProvider()
|
||||
{
|
||||
var HUD = HUDService.hudWeakReferences[hudId].get();
|
||||
var jsterm = HUD.jsterm;
|
||||
var context = jsterm.sandbox.window;
|
||||
var completion;
|
||||
|
||||
// Test if the propertyProvider can be accessed from the jsterm object.
|
||||
ok (jsterm.propertyProvider !== undefined, "JSPropertyProvider is defined");
|
||||
|
||||
completion = jsterm.propertyProvider(context, "thisIsNotDefined");
|
||||
is (completion.matches.length, 0, "no match for 'thisIsNotDefined");
|
||||
|
||||
// This is a case the PropertyProvider can't handle. Should return null.
|
||||
completion = jsterm.propertyProvider(context, "window[1].acb");
|
||||
is (completion, null, "no match for 'window[1].acb");
|
||||
|
||||
// A very advanced completion case.
|
||||
var strComplete =
|
||||
'function a() { }document;document.getElementById(window.locatio';
|
||||
completion = jsterm.propertyProvider(context, strComplete);
|
||||
ok(completion.matches.length == 2, "two matches found");
|
||||
ok(completion.matchProp == "locatio", "matching part is 'test'");
|
||||
ok(completion.matches[0] == "location", "the first match is 'location'");
|
||||
ok(completion.matches[1] == "locationbar", "the second match is 'locationbar'");
|
||||
}
|
||||
|
||||
function testCompletion()
|
||||
{
|
||||
var HUD = HUDService.hudWeakReferences[hudId].get();
|
||||
var jsterm = HUD.jsterm;
|
||||
var input = jsterm.inputNode;
|
||||
|
||||
// Test typing 'docu'.
|
||||
input.value = "docu";
|
||||
input.setSelectionRange(4, 4);
|
||||
jsterm.complete(jsterm.COMPLETE_HINT_ONLY);
|
||||
is(input.value, "document", "'docu' completion");
|
||||
is(input.selectionStart, 4, "start selection is alright");
|
||||
is(input.selectionEnd, 8, "end selection is alright");
|
||||
|
||||
// Test typing 'docu' and press tab.
|
||||
input.value = "docu";
|
||||
input.setSelectionRange(4, 4);
|
||||
jsterm.complete(jsterm.COMPLETE_FORWARD);
|
||||
is(input.value, "document", "'docu' tab completion");
|
||||
is(input.selectionStart, 8, "start selection is alright");
|
||||
is(input.selectionEnd, 8, "end selection is alright");
|
||||
|
||||
// Test typing 'document.getElem'.
|
||||
input.value = "document.getElem";
|
||||
input.setSelectionRange(16, 16);
|
||||
jsterm.complete(jsterm.COMPLETE_HINT_ONLY);
|
||||
is(input.value, "document.getElementById", "'document.getElem' completion");
|
||||
is(input.selectionStart, 16, "start selection is alright");
|
||||
is(input.selectionEnd, 23, "end selection is alright");
|
||||
|
||||
// Test pressing tab another time.
|
||||
jsterm.complete(jsterm.COMPLETE_FORWARD);
|
||||
is(input.value, "document.getElementsByClassName", "'document.getElem' another tab completion");
|
||||
is(input.selectionStart, 16, "start selection is alright");
|
||||
is(input.selectionEnd, 31, "end selection is alright");
|
||||
|
||||
// Test pressing shift_tab.
|
||||
jsterm.complete(jsterm.COMPLETE_BACKWARD);
|
||||
is(input.value, "document.getElementById", "'document.getElem' untab completion");
|
||||
is(input.selectionStart, 16, "start selection is alright");
|
||||
is(input.selectionEnd, 23, "end selection is alright");
|
||||
}
|
||||
|
||||
function testExecutionScope()
|
||||
{
|
||||
content.location.href = TEST_URI;
|
||||
|
@ -533,6 +606,8 @@ function test() {
|
|||
testOutputOrder();
|
||||
testNullUndefinedOutput();
|
||||
testExecutionScope();
|
||||
testCompletion();
|
||||
testPropertyProvider();
|
||||
|
||||
// testUnregister();
|
||||
executeSoon(function () {
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html dir="ltr" xml:lang="en-US" lang="en-US"><head>
|
||||
<title>Property provider test</title>
|
||||
<script>
|
||||
var testObj = {
|
||||
testProp: 'testValue'
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Heads Up Property Provider Test Page</h1>
|
||||
</body>
|
||||
</html>
|
Загрузка…
Ссылка в новой задаче