mirror of
https://github.com/gryf/pentadactyl-pm.git
synced 2025-12-21 07:37:58 +01:00
Document and clean up some of the trickier sections of CompletionContext.
This commit is contained in:
@@ -160,10 +160,10 @@ const CompletionContext = Class("CompletionContext", {
|
|||||||
* @property {CompletionContext} The top-level completion context.
|
* @property {CompletionContext} The top-level completion context.
|
||||||
*/
|
*/
|
||||||
this.top = this;
|
this.top = this;
|
||||||
this.__defineGetter__("incomplete", function () this.contextList.some(function (c) c.parent && c.incomplete));
|
this.__defineGetter__("incomplete", function () this._incomplete || this.contextList.some(function (c) c.parent && c.incomplete));
|
||||||
this.__defineGetter__("waitingForTab", function () this.contextList.some(function (c) c.parent && c.waitingForTab));
|
this.__defineGetter__("waitingForTab", function () this._waitingForTab || this.contextList.some(function (c) c.parent && c.waitingForTab));
|
||||||
this.__defineSetter__("incomplete", function (val) {});
|
this.__defineSetter__("incomplete", function (val) { this._incomplete = val; });
|
||||||
this.__defineSetter__("waitingForTab", function (val) {});
|
this.__defineSetter__("waitingForTab", function (val) { this._waitingForTab = val; });
|
||||||
this.reset();
|
this.reset();
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@@ -315,9 +315,17 @@ const CompletionContext = Class("CompletionContext", {
|
|||||||
this.process = format.process || this.process;
|
this.process = format.process || this.process;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property {string | xml | null}
|
||||||
|
* The message displayed at the head of the completions for the
|
||||||
|
* current context.
|
||||||
|
*/
|
||||||
get message() this._message || (this.waitingForTab ? "Waiting for <Tab>" : null),
|
get message() this._message || (this.waitingForTab ? "Waiting for <Tab>" : null),
|
||||||
set message(val) this._message = val,
|
set message(val) this._message = val,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The prototype object for items returned by {@link items}.
|
||||||
|
*/
|
||||||
get itemPrototype() {
|
get itemPrototype() {
|
||||||
let res = {};
|
let res = {};
|
||||||
function result(quote) {
|
function result(quote) {
|
||||||
@@ -339,18 +347,39 @@ const CompletionContext = Class("CompletionContext", {
|
|||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true when the completions generated by {@link #generate}
|
||||||
|
* must be regenerated. May be set to true to invalidate the current
|
||||||
|
* completions.
|
||||||
|
*/
|
||||||
get regenerate() this._generate && (!this.completions || !this.itemCache[this.key] || this.cache.offset != this.offset),
|
get regenerate() this._generate && (!this.completions || !this.itemCache[this.key] || this.cache.offset != this.offset),
|
||||||
set regenerate(val) { if (val) delete this.itemCache[this.key]; },
|
set regenerate(val) { if (val) delete this.itemCache[this.key]; },
|
||||||
|
|
||||||
get generate() !this._generate ? null : function () {
|
/**
|
||||||
|
* A property which may be set to a function to generate the value
|
||||||
|
* of {@link completions} only when necessary. The generated
|
||||||
|
* completions are linked to the value in {@link #key} and may be
|
||||||
|
* invalidated by setting the {@link #regenerate} property.
|
||||||
|
*/
|
||||||
|
get generate() this._generate || null,
|
||||||
|
set generate(arg) {
|
||||||
|
this.hasItems = true;
|
||||||
|
this._generate = arg;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Generates the item list in {@link #completions} via the
|
||||||
|
* {@link #generate} method if the previously generated value is no
|
||||||
|
* longer valid.
|
||||||
|
*/
|
||||||
|
generateCompletions: function generateCompletions() {
|
||||||
if (this.offset != this.cache.offset || this.lastActivated != this.top.runCount) {
|
if (this.offset != this.cache.offset || this.lastActivated != this.top.runCount) {
|
||||||
this.itemCache = {};
|
this.itemCache = {};
|
||||||
this.cache.offset = this.offset;
|
this.cache.offset = this.offset;
|
||||||
this.lastActivated = this.top.runCount;
|
this.lastActivated = this.top.runCount;
|
||||||
}
|
}
|
||||||
if (!this.itemCache[this.key]) {
|
if (!this.itemCache[this.key])
|
||||||
try {
|
try {
|
||||||
let res = this._generate.call(this);
|
let res = this._generate();
|
||||||
if (res != null)
|
if (res != null)
|
||||||
this.itemCache[this.key] = res;
|
this.itemCache[this.key] = res;
|
||||||
}
|
}
|
||||||
@@ -358,37 +387,42 @@ const CompletionContext = Class("CompletionContext", {
|
|||||||
dactyl.reportError(e);
|
dactyl.reportError(e);
|
||||||
this.message = "Error: " + e;
|
this.message = "Error: " + e;
|
||||||
}
|
}
|
||||||
}
|
// XXX
|
||||||
return this.itemCache[this.key];
|
this.noUpdate = true;
|
||||||
},
|
this.completions = this.itemCache[this.key];
|
||||||
set generate(arg) {
|
this.noUpdate = false;
|
||||||
this.hasItems = true;
|
|
||||||
this._generate = arg;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
get ignoreCase() {
|
get ignoreCase() {
|
||||||
if ("_ignoreCase" in this)
|
if (this._ignoreCase == null) {
|
||||||
return this._ignoreCase;
|
|
||||||
let mode = this.wildcase;
|
let mode = this.wildcase;
|
||||||
if (mode == "match")
|
if (mode == "match")
|
||||||
return this._ignoreCase = false;
|
this._ignoreCase = false;
|
||||||
if (mode == "ignore")
|
if (mode == "ignore")
|
||||||
return this._ignoreCase = true;
|
this._ignoreCase = true;
|
||||||
return this._ignoreCase = !/[A-Z]/.test(this.filter);
|
this._ignoreCase = !/[A-Z]/.test(this.filter);
|
||||||
|
}
|
||||||
|
return this._ignoreCase;
|
||||||
},
|
},
|
||||||
set ignoreCase(val) this._ignoreCase = val,
|
set ignoreCase(val) this._ignoreCase = val,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of all completion items which match the current
|
||||||
|
* filter. The items returned are objects containing one property
|
||||||
|
* for each corresponding property in {@link keys}. The returned
|
||||||
|
* list is generated on-demand from the item list in {@link completions}
|
||||||
|
* or generated by {@link generate}, and is cached as long as no
|
||||||
|
* properties which would invalidate the result are changed.
|
||||||
|
*/
|
||||||
get items() {
|
get items() {
|
||||||
|
// Don't return any items if completions or generator haven't
|
||||||
|
// been set during this completion cycle.
|
||||||
if (!this.hasItems)
|
if (!this.hasItems)
|
||||||
return [];
|
return [];
|
||||||
|
|
||||||
// Regenerate completions if we must
|
// Regenerate completions if we must
|
||||||
if (this.generate) {
|
if (this.generate)
|
||||||
// XXX
|
this.generateCompletions();
|
||||||
this.noUpdate = true;
|
|
||||||
this.completions = this.generate();
|
|
||||||
this.noUpdate = false;
|
|
||||||
}
|
|
||||||
let items = this.completions;
|
let items = this.completions;
|
||||||
|
|
||||||
// Check for cache miss
|
// Check for cache miss
|
||||||
@@ -404,7 +438,7 @@ const CompletionContext = Class("CompletionContext", {
|
|||||||
this.cache.rows = [];
|
this.cache.rows = [];
|
||||||
this.cache.filter = this.filter;
|
this.cache.filter = this.filter;
|
||||||
if (items == null)
|
if (items == null)
|
||||||
return items;
|
return null;
|
||||||
|
|
||||||
let self = this;
|
let self = this;
|
||||||
delete this._substrings;
|
delete this._substrings;
|
||||||
@@ -430,9 +464,10 @@ const CompletionContext = Class("CompletionContext", {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Item prototypes
|
// Item prototypes
|
||||||
|
if (!this.cache.constructed) {
|
||||||
let proto = this.itemPrototype;
|
let proto = this.itemPrototype;
|
||||||
if (!this.cache.constructed)
|
|
||||||
this.cache.constructed = items.map(function (item) ({ __proto__: proto, item: item }));
|
this.cache.constructed = items.map(function (item) ({ __proto__: proto, item: item }));
|
||||||
|
}
|
||||||
|
|
||||||
// Filters
|
// Filters
|
||||||
let filtered = this.filterFunc(this.cache.constructed);
|
let filtered = this.filterFunc(this.cache.constructed);
|
||||||
@@ -452,6 +487,10 @@ const CompletionContext = Class("CompletionContext", {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of all substrings common to all items which
|
||||||
|
* include the current filter.
|
||||||
|
*/
|
||||||
get substrings() {
|
get substrings() {
|
||||||
let items = this.items;
|
let items = this.items;
|
||||||
if (items.length == 0 || !this.hasItems)
|
if (items.length == 0 || !this.hasItems)
|
||||||
@@ -461,10 +500,12 @@ const CompletionContext = Class("CompletionContext", {
|
|||||||
|
|
||||||
let fixCase = this.ignoreCase ? String.toLowerCase : util.identity;
|
let fixCase = this.ignoreCase ? String.toLowerCase : util.identity;
|
||||||
let text = fixCase(items[0].text);
|
let text = fixCase(items[0].text);
|
||||||
|
let filter = fixCase(this.filter);
|
||||||
|
|
||||||
// Exceedingly long substrings cause Gecko to go into convulsions
|
// Exceedingly long substrings cause Gecko to go into convulsions
|
||||||
if (text.length > 100)
|
if (text.length > 100)
|
||||||
text = text.substr(0, 100);
|
text = text.substr(0, 100);
|
||||||
let filter = fixCase(this.filter);
|
|
||||||
if (this.anchored) {
|
if (this.anchored) {
|
||||||
var compare = function compare(text, s) text.substr(0, s.length) == s;
|
var compare = function compare(text, s) text.substr(0, s.length) == s;
|
||||||
var substrings = [text];
|
var substrings = [text];
|
||||||
@@ -480,15 +521,18 @@ const CompletionContext = Class("CompletionContext", {
|
|||||||
start = idx + 1;
|
start = idx + 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
substrings = items.reduce(function (res, item)
|
substrings = items.reduce(function (res, item)
|
||||||
res.map(function (list) {
|
res.map(function (substring) {
|
||||||
var m, len = list.length;
|
// A simple binary search to find the longest substring
|
||||||
var n = list.length;
|
// of the given string which also matches the current
|
||||||
|
// item's text.
|
||||||
|
var m, len = substring.length;
|
||||||
|
var n = substring.length;
|
||||||
var i = 0;
|
var i = 0;
|
||||||
while (n) {
|
while (n) {
|
||||||
m = Math.floor(n / 2);
|
m = Math.floor(n / 2);
|
||||||
let s = list[i + m];
|
let keep = compare(fixCase(item.text), substring.substring(0, i + m));
|
||||||
let keep = compare(fixCase(item.text), list.substring(0, i + m));
|
|
||||||
if (!keep)
|
if (!keep)
|
||||||
len = i + m - 1;
|
len = i + m - 1;
|
||||||
if (!keep || m == 0)
|
if (!keep || m == 0)
|
||||||
@@ -498,9 +542,10 @@ const CompletionContext = Class("CompletionContext", {
|
|||||||
n = n - m;
|
n = n - m;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return len == list.length ? list : list.substr(0, Math.max(len, 0));
|
return len == substring.length ? substring : substring.substr(0, Math.max(len, 0));
|
||||||
}),
|
}),
|
||||||
substrings);
|
substrings);
|
||||||
|
|
||||||
let quote = this.quote;
|
let quote = this.quote;
|
||||||
if (quote)
|
if (quote)
|
||||||
substrings = substrings.map(function (str) quote[0] + quote[1](str));
|
substrings = substrings.map(function (str) quote[0] + quote[1](str));
|
||||||
@@ -532,6 +577,10 @@ const CompletionContext = Class("CompletionContext", {
|
|||||||
this._filter = this._filter.substr(count);
|
this._filter = this._filter.substr(count);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls the {@link #cancel} method of all currently active
|
||||||
|
* sub-contexts.
|
||||||
|
*/
|
||||||
cancelAll: function () {
|
cancelAll: function () {
|
||||||
for (let [, context] in Iterator(this.contextList)) {
|
for (let [, context] in Iterator(this.contextList)) {
|
||||||
if (context.cancel)
|
if (context.cancel)
|
||||||
@@ -571,6 +620,25 @@ const CompletionContext = Class("CompletionContext", {
|
|||||||
yield [i, cache[i] = cache[i] || util.xmlToDom(self.createRow(items[i]), doc)];
|
yield [i, cache[i] = cache[i] || util.xmlToDom(self.createRow(items[i]), doc)];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forks this completion context to create a new sub-context named
|
||||||
|
* as {this.name}/{name}. The new context is automatically advanced
|
||||||
|
* *offset* characters. If *completer* is provided, it is called
|
||||||
|
* with *self* as its 'this' object, the new context as its first
|
||||||
|
* argument, and any subsequent arguments after *completer* as its
|
||||||
|
* following arguments.
|
||||||
|
*
|
||||||
|
* If *completer* is provided, this function returns its return
|
||||||
|
* value, otherwise it returns the new completion context.
|
||||||
|
*
|
||||||
|
* @param {string} name The name of the new context.
|
||||||
|
* @param {number} offset The offset of the new context relative to
|
||||||
|
* the current context's offset.
|
||||||
|
* @param {object} self *completer*'s 'this' object. @optional
|
||||||
|
* @param {function|string} completer A completer function to call
|
||||||
|
* for the new context. If a string is provided, it is
|
||||||
|
* interpreted as a method to access on *self*.
|
||||||
|
*/
|
||||||
fork: function fork(name, offset, self, completer) {
|
fork: function fork(name, offset, self, completer) {
|
||||||
if (typeof completer == "string")
|
if (typeof completer == "string")
|
||||||
completer = self[completer];
|
completer = self[completer];
|
||||||
@@ -591,6 +659,21 @@ const CompletionContext = Class("CompletionContext", {
|
|||||||
return context;
|
return context;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Highlights text in the nsIEditor associated with this completion
|
||||||
|
* context. *length* characters are highlighted from the position
|
||||||
|
* *start*, relative to the current context's offset, with the
|
||||||
|
* selection type *type* as defined in nsISelectionController.
|
||||||
|
*
|
||||||
|
* When called with no arguments, all highlights are removed. When
|
||||||
|
* called with a 0 length, all highlights of type *type* are
|
||||||
|
* removed.
|
||||||
|
*
|
||||||
|
* @param {number} start The position at which to start
|
||||||
|
* highlighting.
|
||||||
|
* @param {number} length The length of the substring to highlight.
|
||||||
|
* @param {string} type The selection type to highlight with.
|
||||||
|
*/
|
||||||
highlight: function highlight(start, length, type) {
|
highlight: function highlight(start, length, type) {
|
||||||
if (arguments.length == 0) {
|
if (arguments.length == 0) {
|
||||||
for (let type in this.selectionTypes)
|
for (let type in this.selectionTypes)
|
||||||
@@ -614,15 +697,35 @@ const CompletionContext = Class("CompletionContext", {
|
|||||||
catch (e) {}
|
catch (e) {}
|
||||||
},
|
},
|
||||||
|
|
||||||
match: function match(str) {
|
/**
|
||||||
return this.matchString(this.filter, str);
|
* Tests the given string for a match against the current filter,
|
||||||
},
|
* taking into account anchoring and case sensitivity rules.
|
||||||
|
*
|
||||||
pushProcessor: function pushProcess(i, fn) {
|
* @param {string} str The string to match.
|
||||||
let next = this.process[i];
|
* @returns {boolean} True if the string matches, false otherwise.
|
||||||
this.process[i] = function (item, text) fn(item, text, next);
|
*/
|
||||||
|
match: function match(str) this.matchString(this.filter, str),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pushes a new output processor onto the processor chain of
|
||||||
|
* {@link #process}. The provided function is called with the item
|
||||||
|
* and text to process along with a reference to the processor
|
||||||
|
* previously installed in the given *index* of {@link #process}.
|
||||||
|
*
|
||||||
|
* @param {number} index The index into {@link #process}.
|
||||||
|
* @param {function(object, string, function)} func The new
|
||||||
|
* processor.
|
||||||
|
*/
|
||||||
|
pushProcessor: function pushProcess(index, func) {
|
||||||
|
let next = this.process[index];
|
||||||
|
this.process[index] = function (item, text) func(item, text, next);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets this completion context and all sub-contexts for use in a
|
||||||
|
* new completion cycle. May only be called on the top-level
|
||||||
|
* context.
|
||||||
|
*/
|
||||||
reset: function reset() {
|
reset: function reset() {
|
||||||
let self = this;
|
let self = this;
|
||||||
if (this.parent)
|
if (this.parent)
|
||||||
@@ -649,9 +752,9 @@ const CompletionContext = Class("CompletionContext", {
|
|||||||
// delete this.contexts[key];
|
// delete this.contexts[key];
|
||||||
for each (let context in this.contexts) {
|
for each (let context in this.contexts) {
|
||||||
context.hasItems = false;
|
context.hasItems = false;
|
||||||
if (context != context.top)
|
|
||||||
context.incomplete = false;
|
context.incomplete = false;
|
||||||
}
|
}
|
||||||
|
this.waitingForTab = false;
|
||||||
this.runCount++;
|
this.runCount++;
|
||||||
for each (let context in this.contextList)
|
for each (let context in this.contextList)
|
||||||
context.lastActivated = this.runCount;
|
context.lastActivated = this.runCount;
|
||||||
|
|||||||
Reference in New Issue
Block a user