michael@0: /* vim:set ts=2 sw=2 sts=2 et tw=80: michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: const cssAutoCompleter = require("devtools/sourceeditor/css-autocompleter"); michael@0: const { AutocompletePopup } = require("devtools/shared/autocomplete-popup"); michael@0: michael@0: const privates = new WeakMap(); michael@0: michael@0: /** michael@0: * Prepares an editor instance for autocompletion, setting up the popup and the michael@0: * CSS completer instance. michael@0: */ michael@0: function setupAutoCompletion(ctx, walker) { michael@0: let { cm, ed, Editor } = ctx; michael@0: michael@0: let win = ed.container.contentWindow.wrappedJSObject; michael@0: michael@0: let completer = null; michael@0: if (ed.config.mode == Editor.modes.css) michael@0: completer = new cssAutoCompleter({walker: walker}); michael@0: michael@0: let popup = new AutocompletePopup(win.parent.document, { michael@0: position: "after_start", michael@0: fixedWidth: true, michael@0: theme: "auto", michael@0: autoSelect: true michael@0: }); michael@0: michael@0: let cycle = (reverse) => { michael@0: if (popup && popup.isOpen) { michael@0: cycleSuggestions(ed, reverse == true); michael@0: return; michael@0: } michael@0: michael@0: return win.CodeMirror.Pass; michael@0: }; michael@0: michael@0: let keyMap = { michael@0: "Tab": cycle, michael@0: "Down": cycle, michael@0: "Shift-Tab": cycle.bind(this, true), michael@0: "Up": cycle.bind(this, true), michael@0: "Enter": () => { michael@0: if (popup && popup.isOpen) { michael@0: if (!privates.get(ed).suggestionInsertedOnce) { michael@0: privates.get(ed).insertingSuggestion = true; michael@0: let {label, preLabel, text} = popup.getItemAtIndex(0); michael@0: let cur = ed.getCursor(); michael@0: ed.replaceText(text.slice(preLabel.length), cur, cur); michael@0: } michael@0: popup.hidePopup(); michael@0: // This event is used in tests michael@0: ed.emit("popup-hidden"); michael@0: return; michael@0: } michael@0: michael@0: return win.CodeMirror.Pass; michael@0: } michael@0: }; michael@0: keyMap[Editor.accel("Space")] = cm => autoComplete(ctx); michael@0: cm.addKeyMap(keyMap); michael@0: michael@0: cm.on("keydown", (cm, e) => onEditorKeypress(ctx, e)); michael@0: ed.on("change", () => autoComplete(ctx)); michael@0: ed.on("destroy", () => { michael@0: cm.off("keydown", (cm, e) => onEditorKeypress(ctx, e)); michael@0: ed.off("change", () => autoComplete(ctx)); michael@0: popup.destroy(); michael@0: popup = null; michael@0: completer = null; michael@0: }); michael@0: michael@0: privates.set(ed, { michael@0: popup: popup, michael@0: completer: completer, michael@0: insertingSuggestion: false, michael@0: suggestionInsertedOnce: false michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * Provides suggestions to autocomplete the current token/word being typed. michael@0: */ michael@0: function autoComplete({ ed, cm }) { michael@0: let private = privates.get(ed); michael@0: let { completer, popup } = private; michael@0: if (!completer || private.insertingSuggestion || private.doNotAutocomplete) { michael@0: private.insertingSuggestion = false; michael@0: return; michael@0: } michael@0: let cur = ed.getCursor(); michael@0: completer.complete(cm.getRange({line: 0, ch: 0}, cur), cur) michael@0: .then(suggestions => { michael@0: if (!suggestions || !suggestions.length || suggestions[0].preLabel == null) { michael@0: private.suggestionInsertedOnce = false; michael@0: popup.hidePopup(); michael@0: ed.emit("after-suggest"); michael@0: return; michael@0: } michael@0: // The cursor is at the end of the currently entered part of the token, like michael@0: // "backgr|" but we need to open the popup at the beginning of the character michael@0: // "b". Thus we need to calculate the width of the entered part of the token michael@0: // ("backgr" here). 4 comes from the popup's left padding. michael@0: michael@0: let cursorElement = cm.display.cursorDiv.querySelector(".CodeMirror-cursor"); michael@0: let left = suggestions[0].preLabel.length * cm.defaultCharWidth() + 4; michael@0: popup.hidePopup(); michael@0: popup.setItems(suggestions); michael@0: popup.openPopup(cursorElement, -1 * left, 0); michael@0: private.suggestionInsertedOnce = false; michael@0: // This event is used in tests. michael@0: ed.emit("after-suggest"); michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * Cycles through provided suggestions by the popup in a top to bottom manner michael@0: * when `reverse` is not true. Opposite otherwise. michael@0: */ michael@0: function cycleSuggestions(ed, reverse) { michael@0: let private = privates.get(ed); michael@0: let { popup, completer } = private; michael@0: let cur = ed.getCursor(); michael@0: private.insertingSuggestion = true; michael@0: if (!private.suggestionInsertedOnce) { michael@0: private.suggestionInsertedOnce = true; michael@0: let firstItem; michael@0: if (reverse) { michael@0: firstItem = popup.getItemAtIndex(popup.itemCount - 1); michael@0: popup.selectPreviousItem(); michael@0: } else { michael@0: firstItem = popup.getItemAtIndex(0); michael@0: if (firstItem.label == firstItem.preLabel && popup.itemCount > 1) { michael@0: firstItem = popup.getItemAtIndex(1); michael@0: popup.selectNextItem(); michael@0: } michael@0: } michael@0: if (popup.itemCount == 1) michael@0: popup.hidePopup(); michael@0: ed.replaceText(firstItem.text.slice(firstItem.preLabel.length), cur, cur); michael@0: } else { michael@0: let fromCur = { michael@0: line: cur.line, michael@0: ch : cur.ch - popup.selectedItem.text.length michael@0: }; michael@0: if (reverse) michael@0: popup.selectPreviousItem(); michael@0: else michael@0: popup.selectNextItem(); michael@0: ed.replaceText(popup.selectedItem.text, fromCur, cur); michael@0: } michael@0: // This event is used in tests. michael@0: ed.emit("suggestion-entered"); michael@0: } michael@0: michael@0: /** michael@0: * onkeydown handler for the editor instance to prevent autocompleting on some michael@0: * keypresses. michael@0: */ michael@0: function onEditorKeypress({ ed, Editor }, event) { michael@0: let private = privates.get(ed); michael@0: michael@0: // Do not try to autocomplete with multiple selections. michael@0: if (ed.hasMultipleSelections()) { michael@0: private.doNotAutocomplete = true; michael@0: private.popup.hidePopup(); michael@0: return; michael@0: } michael@0: michael@0: if ((event.ctrlKey || event.metaKey) && event.keyCode == event.DOM_VK_SPACE) { michael@0: // When Ctrl/Cmd + Space is pressed, two simultaneous keypresses are emitted michael@0: // first one for just the Ctrl/Cmd and second one for combo. The first one michael@0: // leave the private.doNotAutocomplete as true, so we have to make it false michael@0: private.doNotAutocomplete = false; michael@0: return; michael@0: } michael@0: michael@0: if (event.ctrlKey || event.metaKey || event.altKey) { michael@0: private.doNotAutocomplete = true; michael@0: private.popup.hidePopup(); michael@0: return; michael@0: } michael@0: michael@0: switch (event.keyCode) { michael@0: case event.DOM_VK_RETURN: michael@0: private.doNotAutocomplete = true; michael@0: break; michael@0: michael@0: case event.DOM_VK_ESCAPE: michael@0: if (private.popup.isOpen) michael@0: event.preventDefault(); michael@0: case event.DOM_VK_LEFT: michael@0: case event.DOM_VK_RIGHT: michael@0: case event.DOM_VK_HOME: michael@0: case event.DOM_VK_END: michael@0: private.doNotAutocomplete = true; michael@0: private.popup.hidePopup(); michael@0: break; michael@0: michael@0: case event.DOM_VK_BACK_SPACE: michael@0: case event.DOM_VK_DELETE: michael@0: if (ed.config.mode == Editor.modes.css) michael@0: private.completer.invalidateCache(ed.getCursor().line) michael@0: private.doNotAutocomplete = true; michael@0: private.popup.hidePopup(); michael@0: break; michael@0: michael@0: default: michael@0: private.doNotAutocomplete = false; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Returns the private popup. This method is used by tests to test the feature. michael@0: */ michael@0: function getPopup({ ed }) { michael@0: return privates.get(ed).popup; michael@0: } michael@0: michael@0: /** michael@0: * Returns contextual information about the token covered by the caret if the michael@0: * implementation of completer supports it. michael@0: */ michael@0: function getInfoAt({ ed }, caret) { michael@0: let completer = privates.get(ed).completer; michael@0: if (completer && completer.getInfoAt) michael@0: return completer.getInfoAt(ed.getText(), caret); michael@0: michael@0: return null; michael@0: } michael@0: michael@0: // Export functions michael@0: michael@0: module.exports.setupAutoCompletion = setupAutoCompletion; michael@0: module.exports.getAutocompletionPopup = getPopup; michael@0: module.exports.getInfoAt = getInfoAt;