browser/devtools/sourceeditor/autocomplete.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

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;

mercurial