Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
michael@0 | 1 | /* vim:set ts=2 sw=2 sts=2 et tw=80: |
michael@0 | 2 | * This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 5 | |
michael@0 | 6 | const cssAutoCompleter = require("devtools/sourceeditor/css-autocompleter"); |
michael@0 | 7 | const { AutocompletePopup } = require("devtools/shared/autocomplete-popup"); |
michael@0 | 8 | |
michael@0 | 9 | const privates = new WeakMap(); |
michael@0 | 10 | |
michael@0 | 11 | /** |
michael@0 | 12 | * Prepares an editor instance for autocompletion, setting up the popup and the |
michael@0 | 13 | * CSS completer instance. |
michael@0 | 14 | */ |
michael@0 | 15 | function setupAutoCompletion(ctx, walker) { |
michael@0 | 16 | let { cm, ed, Editor } = ctx; |
michael@0 | 17 | |
michael@0 | 18 | let win = ed.container.contentWindow.wrappedJSObject; |
michael@0 | 19 | |
michael@0 | 20 | let completer = null; |
michael@0 | 21 | if (ed.config.mode == Editor.modes.css) |
michael@0 | 22 | completer = new cssAutoCompleter({walker: walker}); |
michael@0 | 23 | |
michael@0 | 24 | let popup = new AutocompletePopup(win.parent.document, { |
michael@0 | 25 | position: "after_start", |
michael@0 | 26 | fixedWidth: true, |
michael@0 | 27 | theme: "auto", |
michael@0 | 28 | autoSelect: true |
michael@0 | 29 | }); |
michael@0 | 30 | |
michael@0 | 31 | let cycle = (reverse) => { |
michael@0 | 32 | if (popup && popup.isOpen) { |
michael@0 | 33 | cycleSuggestions(ed, reverse == true); |
michael@0 | 34 | return; |
michael@0 | 35 | } |
michael@0 | 36 | |
michael@0 | 37 | return win.CodeMirror.Pass; |
michael@0 | 38 | }; |
michael@0 | 39 | |
michael@0 | 40 | let keyMap = { |
michael@0 | 41 | "Tab": cycle, |
michael@0 | 42 | "Down": cycle, |
michael@0 | 43 | "Shift-Tab": cycle.bind(this, true), |
michael@0 | 44 | "Up": cycle.bind(this, true), |
michael@0 | 45 | "Enter": () => { |
michael@0 | 46 | if (popup && popup.isOpen) { |
michael@0 | 47 | if (!privates.get(ed).suggestionInsertedOnce) { |
michael@0 | 48 | privates.get(ed).insertingSuggestion = true; |
michael@0 | 49 | let {label, preLabel, text} = popup.getItemAtIndex(0); |
michael@0 | 50 | let cur = ed.getCursor(); |
michael@0 | 51 | ed.replaceText(text.slice(preLabel.length), cur, cur); |
michael@0 | 52 | } |
michael@0 | 53 | popup.hidePopup(); |
michael@0 | 54 | // This event is used in tests |
michael@0 | 55 | ed.emit("popup-hidden"); |
michael@0 | 56 | return; |
michael@0 | 57 | } |
michael@0 | 58 | |
michael@0 | 59 | return win.CodeMirror.Pass; |
michael@0 | 60 | } |
michael@0 | 61 | }; |
michael@0 | 62 | keyMap[Editor.accel("Space")] = cm => autoComplete(ctx); |
michael@0 | 63 | cm.addKeyMap(keyMap); |
michael@0 | 64 | |
michael@0 | 65 | cm.on("keydown", (cm, e) => onEditorKeypress(ctx, e)); |
michael@0 | 66 | ed.on("change", () => autoComplete(ctx)); |
michael@0 | 67 | ed.on("destroy", () => { |
michael@0 | 68 | cm.off("keydown", (cm, e) => onEditorKeypress(ctx, e)); |
michael@0 | 69 | ed.off("change", () => autoComplete(ctx)); |
michael@0 | 70 | popup.destroy(); |
michael@0 | 71 | popup = null; |
michael@0 | 72 | completer = null; |
michael@0 | 73 | }); |
michael@0 | 74 | |
michael@0 | 75 | privates.set(ed, { |
michael@0 | 76 | popup: popup, |
michael@0 | 77 | completer: completer, |
michael@0 | 78 | insertingSuggestion: false, |
michael@0 | 79 | suggestionInsertedOnce: false |
michael@0 | 80 | }); |
michael@0 | 81 | } |
michael@0 | 82 | |
michael@0 | 83 | /** |
michael@0 | 84 | * Provides suggestions to autocomplete the current token/word being typed. |
michael@0 | 85 | */ |
michael@0 | 86 | function autoComplete({ ed, cm }) { |
michael@0 | 87 | let private = privates.get(ed); |
michael@0 | 88 | let { completer, popup } = private; |
michael@0 | 89 | if (!completer || private.insertingSuggestion || private.doNotAutocomplete) { |
michael@0 | 90 | private.insertingSuggestion = false; |
michael@0 | 91 | return; |
michael@0 | 92 | } |
michael@0 | 93 | let cur = ed.getCursor(); |
michael@0 | 94 | completer.complete(cm.getRange({line: 0, ch: 0}, cur), cur) |
michael@0 | 95 | .then(suggestions => { |
michael@0 | 96 | if (!suggestions || !suggestions.length || suggestions[0].preLabel == null) { |
michael@0 | 97 | private.suggestionInsertedOnce = false; |
michael@0 | 98 | popup.hidePopup(); |
michael@0 | 99 | ed.emit("after-suggest"); |
michael@0 | 100 | return; |
michael@0 | 101 | } |
michael@0 | 102 | // The cursor is at the end of the currently entered part of the token, like |
michael@0 | 103 | // "backgr|" but we need to open the popup at the beginning of the character |
michael@0 | 104 | // "b". Thus we need to calculate the width of the entered part of the token |
michael@0 | 105 | // ("backgr" here). 4 comes from the popup's left padding. |
michael@0 | 106 | |
michael@0 | 107 | let cursorElement = cm.display.cursorDiv.querySelector(".CodeMirror-cursor"); |
michael@0 | 108 | let left = suggestions[0].preLabel.length * cm.defaultCharWidth() + 4; |
michael@0 | 109 | popup.hidePopup(); |
michael@0 | 110 | popup.setItems(suggestions); |
michael@0 | 111 | popup.openPopup(cursorElement, -1 * left, 0); |
michael@0 | 112 | private.suggestionInsertedOnce = false; |
michael@0 | 113 | // This event is used in tests. |
michael@0 | 114 | ed.emit("after-suggest"); |
michael@0 | 115 | }); |
michael@0 | 116 | } |
michael@0 | 117 | |
michael@0 | 118 | /** |
michael@0 | 119 | * Cycles through provided suggestions by the popup in a top to bottom manner |
michael@0 | 120 | * when `reverse` is not true. Opposite otherwise. |
michael@0 | 121 | */ |
michael@0 | 122 | function cycleSuggestions(ed, reverse) { |
michael@0 | 123 | let private = privates.get(ed); |
michael@0 | 124 | let { popup, completer } = private; |
michael@0 | 125 | let cur = ed.getCursor(); |
michael@0 | 126 | private.insertingSuggestion = true; |
michael@0 | 127 | if (!private.suggestionInsertedOnce) { |
michael@0 | 128 | private.suggestionInsertedOnce = true; |
michael@0 | 129 | let firstItem; |
michael@0 | 130 | if (reverse) { |
michael@0 | 131 | firstItem = popup.getItemAtIndex(popup.itemCount - 1); |
michael@0 | 132 | popup.selectPreviousItem(); |
michael@0 | 133 | } else { |
michael@0 | 134 | firstItem = popup.getItemAtIndex(0); |
michael@0 | 135 | if (firstItem.label == firstItem.preLabel && popup.itemCount > 1) { |
michael@0 | 136 | firstItem = popup.getItemAtIndex(1); |
michael@0 | 137 | popup.selectNextItem(); |
michael@0 | 138 | } |
michael@0 | 139 | } |
michael@0 | 140 | if (popup.itemCount == 1) |
michael@0 | 141 | popup.hidePopup(); |
michael@0 | 142 | ed.replaceText(firstItem.text.slice(firstItem.preLabel.length), cur, cur); |
michael@0 | 143 | } else { |
michael@0 | 144 | let fromCur = { |
michael@0 | 145 | line: cur.line, |
michael@0 | 146 | ch : cur.ch - popup.selectedItem.text.length |
michael@0 | 147 | }; |
michael@0 | 148 | if (reverse) |
michael@0 | 149 | popup.selectPreviousItem(); |
michael@0 | 150 | else |
michael@0 | 151 | popup.selectNextItem(); |
michael@0 | 152 | ed.replaceText(popup.selectedItem.text, fromCur, cur); |
michael@0 | 153 | } |
michael@0 | 154 | // This event is used in tests. |
michael@0 | 155 | ed.emit("suggestion-entered"); |
michael@0 | 156 | } |
michael@0 | 157 | |
michael@0 | 158 | /** |
michael@0 | 159 | * onkeydown handler for the editor instance to prevent autocompleting on some |
michael@0 | 160 | * keypresses. |
michael@0 | 161 | */ |
michael@0 | 162 | function onEditorKeypress({ ed, Editor }, event) { |
michael@0 | 163 | let private = privates.get(ed); |
michael@0 | 164 | |
michael@0 | 165 | // Do not try to autocomplete with multiple selections. |
michael@0 | 166 | if (ed.hasMultipleSelections()) { |
michael@0 | 167 | private.doNotAutocomplete = true; |
michael@0 | 168 | private.popup.hidePopup(); |
michael@0 | 169 | return; |
michael@0 | 170 | } |
michael@0 | 171 | |
michael@0 | 172 | if ((event.ctrlKey || event.metaKey) && event.keyCode == event.DOM_VK_SPACE) { |
michael@0 | 173 | // When Ctrl/Cmd + Space is pressed, two simultaneous keypresses are emitted |
michael@0 | 174 | // first one for just the Ctrl/Cmd and second one for combo. The first one |
michael@0 | 175 | // leave the private.doNotAutocomplete as true, so we have to make it false |
michael@0 | 176 | private.doNotAutocomplete = false; |
michael@0 | 177 | return; |
michael@0 | 178 | } |
michael@0 | 179 | |
michael@0 | 180 | if (event.ctrlKey || event.metaKey || event.altKey) { |
michael@0 | 181 | private.doNotAutocomplete = true; |
michael@0 | 182 | private.popup.hidePopup(); |
michael@0 | 183 | return; |
michael@0 | 184 | } |
michael@0 | 185 | |
michael@0 | 186 | switch (event.keyCode) { |
michael@0 | 187 | case event.DOM_VK_RETURN: |
michael@0 | 188 | private.doNotAutocomplete = true; |
michael@0 | 189 | break; |
michael@0 | 190 | |
michael@0 | 191 | case event.DOM_VK_ESCAPE: |
michael@0 | 192 | if (private.popup.isOpen) |
michael@0 | 193 | event.preventDefault(); |
michael@0 | 194 | case event.DOM_VK_LEFT: |
michael@0 | 195 | case event.DOM_VK_RIGHT: |
michael@0 | 196 | case event.DOM_VK_HOME: |
michael@0 | 197 | case event.DOM_VK_END: |
michael@0 | 198 | private.doNotAutocomplete = true; |
michael@0 | 199 | private.popup.hidePopup(); |
michael@0 | 200 | break; |
michael@0 | 201 | |
michael@0 | 202 | case event.DOM_VK_BACK_SPACE: |
michael@0 | 203 | case event.DOM_VK_DELETE: |
michael@0 | 204 | if (ed.config.mode == Editor.modes.css) |
michael@0 | 205 | private.completer.invalidateCache(ed.getCursor().line) |
michael@0 | 206 | private.doNotAutocomplete = true; |
michael@0 | 207 | private.popup.hidePopup(); |
michael@0 | 208 | break; |
michael@0 | 209 | |
michael@0 | 210 | default: |
michael@0 | 211 | private.doNotAutocomplete = false; |
michael@0 | 212 | } |
michael@0 | 213 | } |
michael@0 | 214 | |
michael@0 | 215 | /** |
michael@0 | 216 | * Returns the private popup. This method is used by tests to test the feature. |
michael@0 | 217 | */ |
michael@0 | 218 | function getPopup({ ed }) { |
michael@0 | 219 | return privates.get(ed).popup; |
michael@0 | 220 | } |
michael@0 | 221 | |
michael@0 | 222 | /** |
michael@0 | 223 | * Returns contextual information about the token covered by the caret if the |
michael@0 | 224 | * implementation of completer supports it. |
michael@0 | 225 | */ |
michael@0 | 226 | function getInfoAt({ ed }, caret) { |
michael@0 | 227 | let completer = privates.get(ed).completer; |
michael@0 | 228 | if (completer && completer.getInfoAt) |
michael@0 | 229 | return completer.getInfoAt(ed.getText(), caret); |
michael@0 | 230 | |
michael@0 | 231 | return null; |
michael@0 | 232 | } |
michael@0 | 233 | |
michael@0 | 234 | // Export functions |
michael@0 | 235 | |
michael@0 | 236 | module.exports.setupAutoCompletion = setupAutoCompletion; |
michael@0 | 237 | module.exports.getAutocompletionPopup = getPopup; |
michael@0 | 238 | module.exports.getInfoAt = getInfoAt; |