browser/devtools/sourceeditor/autocomplete.js

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

mercurial