Permalink
Browse files

Issue 55: Auto-completion for Command Editor

  • Loading branch information...
1 parent db8b2ca commit fd8957fbdba57b3d1931c6e1adc286337af996c1 @simonlindholm committed Oct 26, 2012
@@ -127,40 +127,32 @@ Firebug.JSAutoCompleter = function(textBox, completionBox, options)
this.complete = function(context)
{
this.revertValue = null;
- this.createCandidates(context);
- this.showCompletions(false);
+ var offset = this.textBox.selectionStart;
+ if (this.createCandidates(context, this.textBox.value, offset))
+ this.showCompletions(false);
+ else
+ this.hide();
};
/**
* Update the completion base and create completion candidates for the
* current value of the text box.
+ * Return true iff new candidates were constructed.
*/
- this.createCandidates = function(context)
+ this.createCandidates = function(context, value, offset)
{
- var offset = this.textBox.selectionStart;
- if (offset !== this.textBox.value.length)
- {
- this.hide();
- return;
- }
-
- var value = this.textBox.value;
+ if (offset !== value.length)
+ return false;
// Create a simplified expression by redacting contents/normalizing
// delimiters of strings and regexes, to make parsing easier.
// Give up if the syntax is too weird.
var svalue = simplifyExpr(value);
if (svalue === null)
- {
- this.hide();
- return;
- }
+ return false;
if (killCompletions(svalue, value))
- {
- this.hide();
- return;
- }
+ return false;
// Find the expression to be completed.
var parseStart = getExpressionOffset(svalue);
@@ -198,6 +190,7 @@ Firebug.JSAutoCompleter = function(textBox, completionBox, options)
}
this.createCompletions(prop, prevCompletions);
+ return true;
};
/**
@@ -861,22 +854,129 @@ Firebug.JSAutoCompleter = function(textBox, completionBox, options)
// ********************************************************************************************* //
-/**
- * A dummy auto-completer, set as current by CommandLine.setAutoCompleter when
- * no completion is supposed to be done (such as in the large command line,
- * currently, or when there is no context).
- */
-Firebug.EmptyJSAutoCompleter = function()
+Firebug.LargeJSAutoCompleter = function(editor)
{
- this.empty = true;
- this.shutdown = function() {};
- this.hide = function() {};
- this.complete = function() {};
- this.acceptReturn = function() { return true; };
- this.revert = function() { return false; };
- this.handleKeyDown = function() {};
- this.handleKeyPress = function() {};
+ var options = {includeCurrentScope: true};
+ Firebug.JSAutoCompleter.call(this, null, null, options);
+
+ this.editor = editor;
+ this.previous = null;
+
+ /**
+ * Handle a press of the tab key.
+ * Returns 2 if completion succeeded, 1 if it didn't but the caret was still
+ * in a completion area (so the tab shouldn't give rise to an actual tab), or
+ * 0 if it was not intended to complete (e.g., a tab at the start of a line,
+ * or indenting a selection).
+ */
+ this.onTab = function(context, reverse)
+ {
+ var editor = this.editor;
+
+ // If there is a selection, tab either indents or gives an actual tab.
+ if (editor.getSelectedText())
+ return 0;
+
+ var caret = editor.getCaretPosition();
+ var line = caret.line, col = caret.col;
+ var lineStart = editor.getLineStart(line);
+ var lineEnd = editor.getLineEnd(line);
+ var value = editor.getText(lineStart, lineEnd);
+
+ // An actual tab if the previous thing wasn't a JS char.
+ var prevChar = value.charAt(col-1);
+ if (!reJSChar.test(prevChar) && prevChar !== ".")
+ return 0;
+
+ // If the next thing is a JS char, we can't complete (e.g. 'doc|ument'
+ // should not complete to 'document|cument').
+ if (reJSChar.test(value.charAt(col)))
+ return 1;
+
+ var completer = this.completer;
+ var prev = this.previous, compl;
+ if (prev && prev.col === col && prev.line === line && prev.value === value)
+ {
+ // Cycle completion. Restore the input to the original, then set
+ // the completion to the next one in turn.
+ compl = prev.completions;
+ if (reverse)
+ compl.index += compl.list.length-1;
+ else
+ ++compl.index;
+ compl.index %= compl.list.length;
+ value = value.substr(0, prev.baseOffset) + compl.prefix + value.substr(col);
+ col = prev.offset;
+ }
+ else
+ {
+ // Perform a new completion.
+ // For now, only look at the current line.
+ this.hide();
+ var worked = this.createCandidates(context, value.substr(0, col), col);
+ if (!worked || !this.completions)
+ return 1;
+ compl = this.completions;
+ }
+
+ var theCompletion = this.getCurrentCompletion();
+ var baseCol = col - compl.prefix.length;
+ var newCol = baseCol + theCompletion.length;
+ var newValue = value.substr(0, baseCol) + theCompletion + value.substr(col);
+ try
+ {
+ Firebug.CommandEditor.ignoreChanges = true;
+ editor.setText(newValue, lineStart, lineEnd);
+ editor.setCaretOffset(lineStart + baseCol + theCompletion.length);
+ }
+ finally
+ {
+ Firebug.CommandEditor.ignoreChanges = false;
+ }
+
+ this.previous = {
+ col: newCol,
+ line: line,
+ value: newValue,
+ offset: col,
+ baseOffset: baseCol,
+ completions: compl
+ };
+
+ return 2;
+ };
+
+ this.revert = function()
+ {
+ var prev = this.previous;
+ if (!prev)
+ return;
+ var editor = this.editor;
+ var caret = editor.getCaretPosition();
+ var line = caret.line, col = caret.col;
+ var lineStart = editor.getLineStart(line);
+ var lineEnd = editor.getLineEnd(line);
+ var value = editor.getText(lineStart, lineEnd);
+ if (prev.col === col && prev.line === line && prev.value === value)
+ {
+ var newValue = value.substr(0, prev.baseOffset) + prev.completions.prefix + value.substr(col);
+ editor.setText(newValue, lineStart, lineEnd);
+ editor.setCaretOffset(prev.offset);
+ return true;
+ }
+ return false;
+ };
+
+ this.showCompletions = function() {};
+
+ var oldHide = this.hide;
+ this.hide = function()
+ {
+ oldHide.apply(this, arguments);
+ this.previous = null;
+ };
};
+Firebug.LargeJSAutoCompleter.prototype = Object.create(Firebug.JSAutoCompleter.prototype);
// ********************************************************************************************* //
@@ -1806,7 +1906,7 @@ function evalPropChainStep(step, tempExpr, evalChain, out, context)
tempExpr.thisCommand = "window";
tempExpr.command += "(" + link.origCont + ")";
}
- else if (link.name === "")
+ else if (!link.name)
{
// We cannot know about functions without name; try the
// heuristic directly.
@@ -9,8 +9,9 @@ define([
"firebug/lib/locale",
"firebug/lib/css",
"firebug/lib/options",
+ "firebug/console/autoCompleter",
],
-function(Obj, Firebug, Events, Menu, Dom, Locale, Css, Options) {
+function(Obj, Firebug, Events, Menu, Dom, Locale, Css, Options, AutoCompleter) {
// ********************************************************************************************* //
// Constants
@@ -75,17 +76,22 @@ Firebug.CommandEditor = Obj.extend(Firebug.Module,
action: "firebug-cmdEditor-execute",
code: KeyEvent.DOM_VK_RETURN,
accel: true,
- callback: this.onExecute.bind(this),
+ callback: this.onExecute.bind(this)
},{
action: "firebug-cmdEditor-escape",
code: KeyEvent.DOM_VK_ESCAPE,
- callback: this.onEscape.bind(this),
+ callback: this.onEscape.bind(this)
}];
+ this.completer = new Firebug.LargeJSAutoCompleter(this.editor);
+
// Initialize Orion editor.
this.parent = document.getElementById("fbCommandEditor");
this.editor.init(this.parent, config, this.onEditorLoad.bind(this));
+ // Re-fetch the parent, it may have been replaced.
+ this.parent = document.getElementById("fbCommandEditor");
+
if (FBTrace.DBG_COMMANDEDITOR)
FBTrace.sysout("commandEditor: SourceEditor initialized");
},
@@ -97,6 +103,7 @@ Firebug.CommandEditor = Obj.extend(Firebug.Module,
this.editor.removeEventListener(CONTEXT_MENU, this.onContextMenu);
this.editor.removeEventListener(TEXT_CHANGED, this.onTextChanged);
+ this.parent.removeEventListener("keydown", this.onKeyDown, true);
this.editor.destroy();
this.editor = null;
@@ -109,9 +116,13 @@ Firebug.CommandEditor = Obj.extend(Firebug.Module,
onEditorLoad: function()
{
// xxxHonza: Context menu support is going to change in SourceEditor
+ this.onTextChanged = this.onTextChanged.bind(this);
this.editor.addEventListener(CONTEXT_MENU, this.onContextMenu);
this.editor.addEventListener(TEXT_CHANGED, this.onTextChanged);
+ this.onKeyDown = this.onKeyDown.bind(this);
+ this.parent.addEventListener("keydown", this.onKeyDown, true);
+
this.editor.setCaretOffset(this.editor.getCharCount());
Firebug.chrome.applyTextSize(Firebug.textSize);
@@ -134,8 +145,11 @@ Firebug.CommandEditor = Obj.extend(Firebug.Module,
onEscape: function()
{
var context = Firebug.currentContext;
- Firebug.CommandLine.update(context);
- Firebug.CommandLine.cancel(context);
+ if (this.completer.revert())
+ Firebug.CommandLine.update(context);
+ else
+ Firebug.CommandLine.cancel(context);
+ this.completer.hide();
return true;
},
@@ -148,7 +162,27 @@ Firebug.CommandEditor = Obj.extend(Firebug.Module,
if (Firebug.CommandEditor.ignoreChanges)
return;
- Firebug.CommandLine.onCommandLineInput(event);
+ var context = Firebug.currentContext;
+ this.completer.hide();
+ Firebug.CommandLine.update(context);
+ },
+
+ onKeyDown: function(event)
+ {
+ if (event.keyCode !== KeyEvent.DOM_VK_TAB)
+ return;
+ if (event.ctrlKey || event.metaKey || event.altKey)
+ return;
+ var reverse = event.shiftKey;
+ var context = Firebug.currentContext;
+ var ret = this.completer.onTab(context, reverse);
+ if (ret)
+ {
+ // Tab was captured. Save if anything changed.
+ if (ret === 2)
+ Firebug.CommandLine.update(context);
+ Events.cancelEvent(event);
+ }
},
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
@@ -344,23 +378,65 @@ TextEditor.prototype =
Events.removeEventListener(this.textBox, type, callback, true);
},
+ getCaretOffset: function()
+ {
+ return this.textBox.selectionEnd;
+ },
+
+ getCaretPosition: function()
+ {
+ var offset = this.getCaretOffset();
+ var text = this.getText();
+ var pre = text.substr(0, offset);
+ var lines = pre.split("\n");
+ return {
+ line: lines.length-1,
+ col: lines[lines.length-1].length
+ };
+ },
+
+ getLineStart: function(line)
+ {
+ var lines = this.getText().split("\n").slice(0, line);
+ return lines.join("").length + line;
+ },
+
+ getLineEnd: function(line, includeDelimiter)
+ {
+ var lines = this.getText().split("\n").slice(0, line+1);
+ return lines.join("\n").length + (includeDelimiter ? 1 : 0);
+ },
+
setCaretOffset: function(offset)
{
+ this.textBox.setSelectionRange(offset, offset);
},
getCharCount: function()
{
- return this.textBox.value ? this.textBox.value.length : 0;
+ return this.textBox.value.length;
},
- setText: function(text)
+ setText: function(text, start, end)
{
- this.textBox.value = text;
+ if (typeof start === "undefined")
+ {
+ this.textBox.value = text;
+ }
+ else
+ {
+ var value = this.textBox.value;
+ this.textBox.value = value.substr(0, start) + text +
+ (typeof end !== "undefined" ? value.substr(end) : "");
+ }
},
- getText: function()
+ getText: function(start, end)
{
- return this.textBox.value;
+ var text = this.textBox.value;
+ if (typeof start !== "undefined")
+ text = text.substring(start, end);
+ return text;
},
setSelection: function(start, end)
@@ -393,7 +469,7 @@ TextEditor.prototype =
return this.textBox.value.substring(start, end);
}
-}
+};
// ********************************************************************************************* //
// Registration
Oops, something went wrong.

0 comments on commit fd8957f

Please sign in to comment.