diff --git a/common/content/commandline.js b/common/content/commandline.js index 17542af2..b55a244b 100644 --- a/common/content/commandline.js +++ b/common/content/commandline.js @@ -607,7 +607,7 @@ var CommandLine = Module("commandline", { let elem = document.getElementById("dactyl-completions-" + node.id); util.waitFor(bind(this.widgets._ready, null, elem)); - node.completionList = ItemList(elem.id); + node.completionList = ItemList(elem); } return node.completionList; }, @@ -1038,7 +1038,7 @@ var CommandLine = Module("commandline", { this.wildmode = options.get("wildmode"); this.wildtypes = this.wildmode.value; this.itemList = commandline.completionList; - this.itemList.setItems(this.context); + this.itemList.open(this.context); dactyl.registerObserver("events.doneFeeding", this.closure.onDoneFeeding, true); @@ -1051,10 +1051,14 @@ var CommandLine = Module("commandline", { } }, this); this.tabTimer = Timer(0, 0, function tabTell(event) { - this.tab(event.shiftKey, event.altKey && options["altwildmode"]); + let tabCount = this.tabCount; + this.tabCount = 0; + this.tab(tabCount, event.altKey && options["altwildmode"]); }, this); }, + tabCount: 0, + cleanup: function () { dactyl.unregisterObserver("events.doneFeeding", this.closure.onDoneFeeding); this.previewClear(); @@ -1071,8 +1075,15 @@ var CommandLine = Module("commandline", { this.ignoredCount = 0; }, + onTab: function onTab(event) { + this.tabCount += event.shiftKey ? -1 : 1; + this.tabTimer.tell(event); + }, + UP: {}, DOWN: {}, + CTXT_UP: {}, + CTXT_DOWN: {}, PAGE_UP: {}, PAGE_DOWN: {}, RESET: null, @@ -1162,14 +1173,16 @@ var CommandLine = Module("commandline", { let value = this.completion; if (util.compareIgnoreCase(value, substring.substr(0, value.length))) return; + substring = substring.substr(value.length); this.removeSubstring = substring; - let node = util.xmlToDom({substring}, - document); - let start = this.caret; - this.editor.insertNode(node, this.editor.rootElement, 1); - this.caret = start; + let node = DOM.fromXML({substring}, + document); + + this.withSavedValues(["caret"], function () { + this.editor.insertNode(node, this.editor.rootElement, 1); + }); }, previewClear: function previewClear() { @@ -1199,7 +1212,7 @@ var CommandLine = Module("commandline", { this.suffix = this.context.value.substring(this.caret); if (show) { - this.itemList.reset(); + this.itemList.update(); if (this.haveType("list")) this.itemList.visible = true; this.selected = null; @@ -1215,25 +1228,25 @@ var CommandLine = Module("commandline", { this.value = value.substring(this.start, this.caret); this.suffix = value.substring(this.caret); - this.itemList.reset(); + this.itemList.update(); this.itemList.selectItem(this.selected); this.preview(); }, - select: function select(idx) { + select: function select(idx, count) { switch (idx) { case this.UP: if (this.selected == null) - idx = -2; + idx = -1 - count; else - idx = this.selected - 1; + idx = this.selected - count; break; case this.DOWN: if (this.selected == null) - idx = 0; + idx = count - 1; else - idx = this.selected + 1; + idx = this.selected + count; break; case this.RESET: idx = null; @@ -1287,7 +1300,7 @@ var CommandLine = Module("commandline", { tabs: [], - tab: function tab(reverse, wildmode) { + tab: function tab(count, wildmode) { this.autocompleteTimer.flush(); this.ignoredCount = 0; @@ -1299,12 +1312,12 @@ var CommandLine = Module("commandline", { if (this.context.waitingForTab || this.wildIndex == -1) this.complete(true, true); - this.tabs.push([reverse, wildmode || options["wildmode"]]); + this.tabs.push([count, wildmode || options["wildmode"]]); if (this.waiting) return; while (this.tabs.length) { - [reverse, this.wildtypes] = this.tabs.shift(); + [count, this.wildtypes] = this.tabs.shift(); this.wildIndex = Math.min(this.wildIndex, this.wildtypes.length - 1); switch (this.wildtype.replace(/.*:/, "")) { @@ -1319,7 +1332,7 @@ var CommandLine = Module("commandline", { } // Fallthrough case "full": - this.select(reverse ? this.UP : this.DOWN); + this.select(count < 0 ? this.UP : this.DOWN, Math.abs(count)); break; } @@ -1510,13 +1523,13 @@ var CommandLine = Module("commandline", { bind(["", ""], "Select the next matching completion item", function ({ keypressEvents, self }) { dactyl.assert(self.completions); - self.completions.tabTimer.tell(keypressEvents[0]); + self.completions.onTab(keypressEvents[0]); }); bind(["", ""], "Select the previous matching completion item", function ({ keypressEvents, self }) { dactyl.assert(self.completions); - self.completions.tabTimer.tell(keypressEvents[0]); + self.completions.onTab(keypressEvents[0]); }); bind(["", ""], "Delete the previous character", @@ -1585,25 +1598,339 @@ var CommandLine = Module("commandline", { }); /** - * The list which is used for the completion box (and QuickFix window in - * future). + * The list which is used for the completion box. * * @param {string} id The id of the iframe which will display the list. It * must be in its own container element, whose height it will update as * necessary. */ -var ItemList = Class("ItemList", { - init: function init(id) { - this._completionElements = []; - var iframe = document.getElementById(id); +var NewItemList = Class("ItemList", { + CONTEXT_LINES: 3, + + init: function init(frame) { + this.frame = frame; + + this.doc = frame.contentDocument; + this.win = frame.contentWindow; + this.body = this.doc.body; + this.container = frame.parentNode; + + highlight.highlightNode(this.doc.body, "Comp"); + + this._resize = Timer(20, 400, function _resize() { + if (this.visible) + this.resize(); + }, this); + }, + + get rootXML() +
+
+
{_("completion.noCompletions")}
+
+
+ +
{ + template.map(util.range(0, options["maxitems"] * 2), function (i) +
  • ~
  • ) + }
    +
    + .elements(), + + get visible() !this.container.collapsed, + set visible(val) this.container.collapsed = !val, + + get activeGroups() this.context.contextList + .filter(function (c) c.message || c.incomplete + || c.hasItems && c.items.length) + .map(this.getGroup, this), + + open: function open(context) { + util.dump("\n\n\n\n"); + util.dump("OPEN()"); + this.context = context; + this.nodes = {x:1}; + this.maxItems = options["maxitems"]; + + DOM(this.rootXML, this.doc, this.nodes) + .appendTo(DOM(this.body).empty()); + + this.update(); + this.visible = true; + }, + + update: function update() { + util.dump("\n\n"); + util.dump("UPDATE()"); + DOM(this.nodes.completions).empty(); + + let groups = this.activeGroups; + let container = DOM(this.nodes.completions); + for each (let group in groups) { + group.reset(); + container.append(group.nodes.root); + } + + DOM(this.nodes.noCompletions).toggle(!groups.length); + + this.select(groups[0] && groups[0].context, null); + + this._resize.tell(); + }, + + draw: function draw() { + util.dump("DRAW()"); + for each (let group in this.activeGroups) + group.draw(); + + // We need to collect all of the rescrolling functions in + // one go, as the height calculation that they need to do + // would force a reflow after each DOM modification. + this.activeGroups.filter(function (g) !g.collapsed) + .map(function (g) g.rescrollFunc) + .forEach(function (f) f()); + }, + + minHeight: 0, + resize: function resize() { + util.dump("RESIZE()"); + let { completions, root } = this.nodes; + + if (!this.visible) + root.style.minWidth = document.getElementById("dactyl-commandline").scrollWidth + "px"; + + this.minHeight = Math.max(this.minHeight, + this.win.scrollY + DOM(completions).rect.bottom); + + if (!this.visible) + root.style.minWidth = ""; + + // FIXME: Belongs elsewhere. + mow.resize(false, Math.max(0, this.minHeight - this.container.height)); + + this.container.height = this.minHeight; + this.container.height -= mow.spaceNeeded; + mow.resize(false); + this.timeout(function () { + this.container.height -= mow.spaceNeeded; + }); + }, + + select: function select(context, index, position) { + util.dump("SELECT()"); + let group = this.getGroup(context); + + if (this.selectedGroup && (!group || group != this.selectedGroup)) + this.selectedGroup.selectedIdx = null; + + this.selectedGroup = group; + + if (group) + group.selectedIdx = index; + + if (position != null) + this.selectionPosition = position; + + let groups = this.activeGroups; + if (groups.length) { + group = group || groups[0]; + let idx = groups.indexOf(group); + + let count = this.maxItems; + group.count = Math.min((group.selectedIdx || 0) + this.CONTEXT_LINES, + group.itemCount, count); + count -= group.count; + + for (let i = idx - 1; i >= 0; i--) { + let group = groups[i]; + group.count = Math.min(group.itemCount, count); + count -= group.count; + } + + let n = group.count; + group.count = Math.min(group.count + count, group.itemCount); + count -= group.count - n; + + for (let i = idx + 1; i < groups.length; i++) { + let group = groups[i]; + group.count = Math.min(group.itemCount, count); + count -= group.count; + } + + for (let [i, group] in Iterator(groups)) { + group.collapsed = group.count == 0; + if (i < idx) + group.range = ItemList.Range(group.itemCount - group.count, + group.itemCount); + else if (i > idx) + group.range = ItemList.Range(0, group.count); + else { + let end = Math.max(group.count, + Math.min(group.selectedIdx + this.CONTEXT_LINES, + group.itemCount)); + group.range = ItemList.Range(end - group.count, end); + } + } + } + this.draw(); + }, + + selectItem: function selectItem(idx) { + if (idx != null) + for each (var group in this.activeGroups) { + if (idx < group.itemCount) + break; + idx -= group.itemCount; + } + + this.select(group && group.context, idx); + }, + + getGroup: function getGroup(context) context && + context.getCache("itemlist-group", bind("Group", ItemList, this, context)) +}, { + WAITING_MESSAGE: _("completion.generating"), + + Group: Class("ItemList.Group", { + init: function init(parent, context) { + this.parent = parent; + this.context = context; + }, + + get rootXML() +
    +
    + { this.context.createRow(this.context.title || [], "CompTitle") } +
    +
    +
    +
    +
    {this.context.message}
    +
    +
    +
    +
    {ItemList.WAITING_MESSAGE}
    +
    +
    +
    , + + get doc() this.parent.doc, + get win() this.parent.win, + get maxItems() this.parent.maxItems, + + get itemCount() this.context.items.length, + + get rescrollFunc() { + let container = this.nodes.itemsContainer; + let pos = DOM(container).rect.top; + let start = DOM(this.getItem(this.range.start)).rect.top; + let height = DOM(this.getItem(this.range.end - 1)).rect.bottom - start || 0; + let scroll = start + container.scrollTop - pos; + return function () { + container.scrollTop = scroll; + container.style.height = height + "px"; + } + }, + + draw: function draw() { + util.dump("draw(" + [this.collapsed, this.itemCount, this.count] + ") [" + + (!this.collapsed && [this.range.start, this.range.end]) + ") [" + + [this.generatedRange.start, + this.generatedRange.end] + ") " + + (!this.collapsed && this.generatedRange.contains(this.range))); + + DOM(this.nodes.contents).toggle(!this.collapsed); + if (this.collapsed) + return; + + DOM(this.nodes.message).toggle(this.context.message && this.range.start == 0); + DOM(this.nodes.waiting).toggle(this.context.incomplete && this.range.end < this.itemCount); + DOM(this.nodes.up).toggle(this.range.start > 0); + DOM(this.nodes.down).toggle(this.range.end < this.itemCount); + + if (!this.generatedRange.contains(this.range)) { + if (this.generatedRange.end == 0) + var [start, end] = this.range; + else { + start = this.range.start - (this.range.start <= this.generatedRange.start + ? this.maxItems / 2 : 0); + end = this.range.end + (this.range.end > this.generatedRange.end + ? this.maxItems / 2 : 0); + } + util.dump(" refill [" + [start,end] + ")"); + + let range = ItemList.Range(Math.max(0, start), Math.min(this.itemCount, end)); + + let first; + for (let [i, row] in this.context.getRows(this.generatedRange.start, + this.generatedRange.end, + this.doc)) + if (!range.contains(i)) + DOM(row).remove(); + else if (!first) + first = row; + + let container = DOM(this.nodes.items); + let before = first ? DOM(first).closure.before + : DOM(this.nodes.items).closure.append; + + for (let [i, row] in this.context.getRows(range.start, range.end, + this.doc)) + if (i < this.generatedRange.start) + before(row); + else if (i >= this.generatedRange.end) + container.append(row); + + this.generatedRange = range; + } + }, + + reset: function reset() { + this.nodes = {}; + this.generatedRange = ItemList.Range(0, 0); + + DOM.fromXML(this.rootXML, this.doc, this.nodes); + }, + + getItem: function getItem(idx) this.context.getRow(idx), + + get selectedItem() this.getItem(this._selectedIdx), + + get selectedIdx() this._selectedIdx, + set selectedIdx(idx) { + if (this.selectedItem) + DOM(this.selectedItem).attr("selected", null); + + this._selectedIdx = idx; + + if (this.selectedItem) + DOM(this.selectedItem).attr("selected", true); + } + }), + + Range: Class.Memoize(function () { + let Range = Struct("ItemList.Range", "start", "end"); + update(Range.prototype, { + contains: function contains(idx) + typeof idx == "number" ? idx >= this.start && idx < this.end + : this.contains(idx.start) && + idx.end >= this.start && idx.end <= this.end + }); + return Range; + }) +}); + +var ItemList = Class("ItemList", { + init: function init(iframe) { + this._completionElements = []; this._doc = iframe.contentDocument; this._win = iframe.contentWindow; this._container = iframe.parentNode; - this._doc.documentElement.id = id + "-top"; - this._doc.body.id = id + "-content"; + this._doc.documentElement.id = iframe.id + "-top"; + this._doc.body.id = iframe.id + "-content"; this._doc.body.className = iframe.className + "-content"; this._doc.body.appendChild(this._doc.createTextNode("")); this._doc.body.style.borderTop = "1px solid black"; // FIXME: For cases where completions/MOW are shown at once, or ls=0. Should use :highlight. @@ -1647,7 +1974,7 @@ var ItemList = Class("ItemList", { _init: function _init() { this._div = this._dom( -
    +
    {_("completion.noCompletions")}
    @@ -1676,7 +2003,9 @@ var ItemList = Class("ItemList", {
    -
    +
    +
    +
    {ItemList.WAITING_MESSAGE}
    , context.cache.nodes); @@ -1776,7 +2105,7 @@ var ItemList = Class("ItemList", { get visible() !this._container.collapsed, set visible(val) this._container.collapsed = !val, - reset: function reset(brief) { + update: function reset(brief) { this._startIndex = this._endIndex = this._selIndex = -1; this._div = null; if (!brief) @@ -1787,20 +2116,22 @@ var ItemList = Class("ItemList", { setItems: function setItems(newItems, selectedItem) { if (this._selItem > -1) this._getCompletion(this._selItem).removeAttribute("selected"); + if (this._container.collapsed) { this._minHeight = 0; this._container.height = 0; } + this._startIndex = this._endIndex = this._selIndex = -1; this._items = newItems; - this.reset(true); + this.update(true); if (typeof selectedItem == "number") { this.selectItem(selectedItem); this.visible = true; } }, + get open() this.closure.setItems, - // select index, refill list if necessary selectItem: function selectItem(index) { //let now = Date.now(); @@ -1848,7 +2179,7 @@ var ItemList = Class("ItemList", { onKeyPress: function onKeyPress(event) false }, { - WAITING_MESSAGE: _("completion.generating") + WAITING_MESSAGE: _("completion.generating"), }); // vim: set fdm=marker sw=4 ts=4 et: diff --git a/common/content/mow.js b/common/content/mow.js index 7bf40a9f..32dbe853 100644 --- a/common/content/mow.js +++ b/common/content/mow.js @@ -21,11 +21,9 @@ var MOW = Module("mow", { if (modes.have(modes.OUTPUT_MULTILINE)) { this.resize(true); - if (options["more"] && this.canScroll(1)) { + if (options["more"] && this.canScroll(1)) // start the last executed command's output at the top of the screen - let elements = this.document.getElementsByClassName("ex-command-output"); - DOM(elements[elements.length - 1]).scrollIntoView(true); - } + DOM(this.document.body.lastElementChild).scrollIntoView(true); else this.body.scrollTop = this.body.scrollHeight; @@ -122,10 +120,12 @@ var MOW = Module("mow", { // after interpolated data. XML.ignoreWhitespace = XML.prettyPrinting = false; + highlightGroup = "CommandOutput " + (highlight || ""); + if (isObject(data) && !isinstance(data, _)) { this.lastOutput = null; - var output = DOM(
    , + var output = DOM(
    , this.document); data.document = this.document; try { @@ -139,7 +139,7 @@ var MOW = Module("mow", { } else { let style = isString(data) ? "pre-wrap" : "nowrap"; - this.lastOutput =
    {data}
    ; + this.lastOutput =
    {data}
    ; var output = DOM(this.lastOutput, this.document); } diff --git a/common/modules/base.jsm b/common/modules/base.jsm index 5b337207..77b35d81 100644 --- a/common/modules/base.jsm +++ b/common/modules/base.jsm @@ -868,6 +868,7 @@ Class.Memoize = function Memoize(getter, wait) let done = false; if (wait) + // Crazy, yeah, I know. -- Kris this.get = function replace() { let obj = this.instance || this; Object.defineProperty(obj, key, { @@ -892,7 +893,7 @@ Class.Memoize = function Memoize(getter, wait) return this[key]; }; else - this.get = function replace() { + this.get = function g_Memoize() { let obj = this.instance || this; try { Class.replaceProperty(obj, key, null); @@ -903,7 +904,7 @@ Class.Memoize = function Memoize(getter, wait) } }; - this.set = function replace(val) Class.replaceProperty(this.instance || this, val); + this.set = function s_Memoize(val) Class.replaceProperty(this.instance || this, key, val); } }); @@ -1227,6 +1228,8 @@ var StructBase = Class("StructBase", Array, { this[i] = arguments[i]; }, + get toStringParams() this, + clone: function struct_clone() this.constructor.apply(null, this.slice()), closure: Class.Property(Object.getOwnPropertyDescriptor(Class.prototype, "closure")), diff --git a/common/modules/buffer.jsm b/common/modules/buffer.jsm index cdd13da0..2ad6b15d 100644 --- a/common/modules/buffer.jsm +++ b/common/modules/buffer.jsm @@ -289,7 +289,7 @@ var Buffer = Module("Buffer", { * @param {Node} elem The element to focus. */ focusElement: function focusElement(elem) { - let { dactyl } = this.modules; + let { Editor, dactyl } = this.modules; let win = elem.ownerDocument && elem.ownerDocument.defaultView || elem; overlay.setData(elem, "focus-allowed", true); diff --git a/common/modules/completion.jsm b/common/modules/completion.jsm index b5178014..a147544d 100644 --- a/common/modules/completion.jsm +++ b/common/modules/completion.jsm @@ -633,25 +633,33 @@ var CompletionContext = Class("CompletionContext", { return iter.map(util.range(start, end, step), function (i) items[i]); }, + getRow: function getRow(idx) this.cache.rows && this.cache.rows[idx], + getRows: function getRows(start, end, doc) { let self = this; let items = this.items; let cache = this.cache.rows; let step = start > end ? -1 : 1; + start = Math.max(0, start || 0); end = Math.min(items.length, end != null ? end : items.length); - for (let i in util.range(start, end, step)) - try { - yield [i, cache[i] = cache[i] || util.xmlToDom(self.createRow(items[i]), doc)]; - } - catch (e) { - util.reportError(e); - yield [i, cache[i] = cache[i] || util.xmlToDom( -
    -
  • {items[i].text} 
  • -
  • {e} 
  • -
    , doc)]; - } + + for (let i in util.range(start, end, step)) { + if (!cache[i]) + try { + cache[i] = util.xmlToDom(self.createRow(items[i]), doc); + } + catch (e) { + util.reportError(e); + cache[i] = util.xmlToDom( +
    +
  • {items[i].text} 
  • +
  • {e} 
  • +
    , doc); + } + + yield [i, cache[i]]; + } }, /** diff --git a/common/modules/contexts.jsm b/common/modules/contexts.jsm index f5744d50..0cf60472 100644 --- a/common/modules/contexts.jsm +++ b/common/modules/contexts.jsm @@ -94,7 +94,7 @@ var Contexts = Module("contexts", { cleanup: function () { for each (let module in this.pluginModules) - util.trapErrors("cleanup", module); + util.trapErrors("unload", module); this.pluginModules = {}; }, diff --git a/common/modules/dom.jsm b/common/modules/dom.jsm index c21fddb5..3ef85156 100644 --- a/common/modules/dom.jsm +++ b/common/modules/dom.jsm @@ -36,10 +36,13 @@ function BooleanAttribute(attr) ({ * change in the near future. */ var DOM = Class("DOM", { - init: function init(val, context) { + init: function init(val, context, nodes) { let self; let length = 0; + if (nodes) + this.nodes = nodes; + if (context instanceof Ci.nsIDOMDocument) this.document = context; @@ -48,7 +51,7 @@ var DOM = Class("DOM", { if (val == null) ; - else if (typeof val == "xml") + else if (typeof val == "xml" && context instanceof Ci.nsIDOMDocument) this[length++] = DOM.fromXML(val, context, this.nodes); else if (val instanceof Ci.nsIDOMNode || val instanceof Ci.nsIDOMWindow) this[length++] = val; @@ -58,6 +61,8 @@ var DOM = Class("DOM", { else if ("__iterator__" in val || isinstance(val, ["Iterator", "Generator"])) for (let elem in val) this[length++] = elem; + else + this[length++] = val; this.length = length; return self || this; @@ -130,19 +135,22 @@ var DOM = Class("DOM", { }, self || this); let dom = this; - function munge(val) { + function munge(val, container, idx) { if (val instanceof Ci.nsIDOMRange) return val.extractContents(); if (val instanceof Ci.nsIDOMNode) return val; - if (typeof val == "xml") + if (typeof val == "xml") { val = dom.constructor(val, dom.document); + if (container) + container[idx] = val[0]; + } if (isObject(val) && "length" in val) { let frag = dom.document.createDocumentFragment(); for (let i = 0; i < val.length; i++) - frag.appendChild(munge(val[i])); + frag.appendChild(munge(val[i], val, i)); return frag; } return val; diff --git a/common/modules/util.jsm b/common/modules/util.jsm index fb7f1878..ae40dd21 100644 --- a/common/modules/util.jsm +++ b/common/modules/util.jsm @@ -28,7 +28,7 @@ var FailedAssertion = Class("FailedAssertion", ErrorBase, { noTrace: true }); -var Point = Struct("x", "y"); +var Point = Struct("Point", "x", "y"); var wrapCallback = function wrapCallback(fn, isEvent) { if (!fn.wrapper) diff --git a/common/skin/dactyl.css b/common/skin/dactyl.css index f4db095c..f8726865 100644 --- a/common/skin/dactyl.css +++ b/common/skin/dactyl.css @@ -68,6 +68,10 @@ input[type=file][dactyl|highlight~=HintElem] { line-height: 1.5em !important; } +.completion-items-container { + overflow: hidden; +} + .td-span { display: inline-block; overflow: visible; @@ -191,11 +195,9 @@ statusbarpanel { /* MOW */ -.dactyl-completions, -#dactyl-multiline-output, -#dactyl-multiline-input { - background-color: white; - color: black; +#dactyl-commandline-prompt *, +#dactyl-commandline-command { + font: inherit; } .dactyl-completions-content, @@ -206,11 +208,6 @@ statusbarpanel { margin: 0px; } -#dactyl-commandline-prompt *, -#dactyl-commandline-command { - font: inherit; -} - .dactyl-completions-content table, #dactyl-multiline-output-content table { white-space: inherit; diff --git a/common/skin/global-styles.css b/common/skin/global-styles.css index adcf325c..5f175649 100644 --- a/common/skin/global-styles.css +++ b/common/skin/global-styles.css @@ -84,6 +84,9 @@ CmdInput;.dactyl-commandline-command CmdOutput /* The output of commands executed by :run */ \ white-space: pre; +Comp;;;FontFixed,Normal /* The completion window */ \ + margin: 0; border-top: 1px solid black; + CompGroup /* Item group in completion output */ CompGroup:not(:first-of-type) margin-top: .5em; CompGroup:last-of-type padding-bottom: 1.5ex;