|
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; |