| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351 |
- /* ***** BEGIN LICENSE BLOCK *****
- * Distributed under the BSD license:
- *
- * Copyright (c) 2012, Ajax.org B.V.
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- * * Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- * * Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- * * Neither the name of Ajax.org B.V. nor the
- * names of its contributors may be used to endorse or promote products
- * derived from this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *
- * ***** END LICENSE BLOCK ***** */
- define(function(require, exports, module) {
- "use strict";
- var HashHandler = require("./keyboard/hash_handler").HashHandler;
- var AcePopup = require("./autocomplete/popup").AcePopup;
- var util = require("./autocomplete/util");
- var event = require("./lib/event");
- var lang = require("./lib/lang");
- var snippetManager = require("./snippets").snippetManager;
- var Autocomplete = function() {
- this.keyboardHandler = new HashHandler();
- this.keyboardHandler.bindKeys(this.commands);
- this.blurListener = this.blurListener.bind(this);
- this.changeListener = this.changeListener.bind(this);
- this.mousedownListener = this.mousedownListener.bind(this);
- this.mousewheelListener = this.mousewheelListener.bind(this);
-
- this.changeTimer = lang.delayedCall(function() {
- this.updateCompletions(true);
- }.bind(this))
- };
- (function() {
- this.$init = function() {
- this.popup = new AcePopup(document.body || document.documentElement);
- this.popup.on("click", function(e) {
- this.insertMatch();
- e.stop();
- }.bind(this));
- };
- this.openPopup = function(editor, prefix, keepPopupPosition) {
- if (!this.popup)
- this.$init();
- this.popup.setData(this.completions.filtered);
- var renderer = editor.renderer;
- if (!keepPopupPosition) {
- this.popup.setRow(0);
- this.popup.setFontSize(editor.getFontSize());
- var lineHeight = renderer.layerConfig.lineHeight;
-
- var pos = renderer.$cursorLayer.getPixelPosition(this.base, true);
- pos.left -= this.popup.getTextLeftOffset();
-
- var rect = editor.container.getBoundingClientRect();
- pos.top += rect.top - renderer.layerConfig.offset;
- pos.left += rect.left - editor.renderer.scrollLeft;
- pos.left += renderer.$gutterLayer.gutterWidth;
- this.popup.show(pos, lineHeight);
- }
- };
- this.detach = function() {
- this.editor.keyBinding.removeKeyboardHandler(this.keyboardHandler);
- this.editor.off("changeSelection", this.changeListener);
- this.editor.off("blur", this.changeListener);
- this.editor.off("mousedown", this.mousedownListener);
- this.editor.off("mousewheel", this.mousewheelListener);
- this.changeTimer.cancel();
-
- if (this.popup)
- this.popup.hide();
- this.activated = false;
- this.completions = this.base = null;
- };
- this.changeListener = function(e) {
- var cursor = this.editor.selection.lead;
- if (cursor.row != this.base.row || cursor.column < this.base.column) {
- this.detach();
- }
- if (this.activated)
- this.changeTimer.schedule();
- else
- this.detach();
- };
- this.blurListener = function() {
- if (document.activeElement != this.editor.textInput.getElement())
- this.detach();
- };
- this.mousedownListener = function(e) {
- this.detach();
- };
- this.mousewheelListener = function(e) {
- this.detach();
- };
- this.goTo = function(where) {
- var row = this.popup.getRow();
- var max = this.popup.session.getLength() - 1;
- switch(where) {
- case "up": row = row < 0 ? max : row - 1; break;
- case "down": row = row >= max ? -1 : row + 1; break;
- case "start": row = 0; break;
- case "end": row = max; break;
- }
- this.popup.setRow(row);
- };
- this.insertMatch = function(data) {
- if (!data)
- data = this.popup.getData(this.popup.getRow());
- if (!data)
- return false;
- if (data.completer && data.completer.insertMatch) {
- data.completer.insertMatch(this.editor);
- } else {
- if (this.completions.filterText) {
- var ranges = this.editor.selection.getAllRanges();
- for (var i = 0, range; range = ranges[i]; i++) {
- range.start.column -= this.completions.filterText.length;
- this.editor.session.remove(range);
- }
- }
- if (data.snippet)
- snippetManager.insertSnippet(this.editor, data.snippet);
- else
- this.editor.execCommand("insertstring", data.value || data);
- }
- this.detach();
- };
- this.commands = {
- "Up": function(editor) { editor.completer.goTo("up"); },
- "Down": function(editor) { editor.completer.goTo("down"); },
- "Ctrl-Up|Ctrl-Home": function(editor) { editor.completer.goTo("start"); },
- "Ctrl-Down|Ctrl-End": function(editor) { editor.completer.goTo("end"); },
- "Esc": function(editor) { editor.completer.detach(); },
- "Space": function(editor) { editor.completer.detach(); editor.insert(" ");},
- "Return": function(editor) { editor.completer.insertMatch(); },
- "Shift-Return": function(editor) { editor.completer.insertMatch(true); },
- "Tab": function(editor) { editor.completer.insertMatch(); },
- "PageUp": function(editor) { editor.completer.popup.gotoPageUp(); },
- "PageDown": function(editor) { editor.completer.popup.gotoPageDown(); }
- };
- this.gatherCompletions = function(editor, callback) {
- var session = editor.getSession();
- var pos = editor.getCursorPosition();
-
- var line = session.getLine(pos.row);
- var prefix = util.retrievePrecedingIdentifier(line, pos.column);
-
- this.base = editor.getCursorPosition();
- this.base.column -= prefix.length;
- var matches = [];
- util.parForEach(editor.completers, function(completer, next) {
- completer.getCompletions(editor, session, pos, prefix, function(err, results) {
- if (!err)
- matches = matches.concat(results);
- next();
- });
- }, function() {
- callback(null, {
- prefix: prefix,
- matches: matches
- });
- });
- return true;
- };
- this.showPopup = function(editor) {
- if (this.editor)
- this.detach();
-
- this.activated = true;
- this.editor = editor;
- if (editor.completer != this) {
- if (editor.completer)
- editor.completer.detach();
- editor.completer = this;
- }
- editor.keyBinding.addKeyboardHandler(this.keyboardHandler);
- editor.on("changeSelection", this.changeListener);
- editor.on("blur", this.blurListener);
- editor.on("mousedown", this.mousedownListener);
- editor.on("mousewheel", this.mousewheelListener);
-
- this.updateCompletions();
- };
-
- this.updateCompletions = function(keepPopupPosition) {
- if (keepPopupPosition && this.base && this.completions) {
- var pos = this.editor.getCursorPosition();
- var prefix = this.editor.session.getTextRange({start: this.base, end: pos});
- if (prefix == this.completions.filterText)
- return;
- this.completions.setFilter(prefix);
- if (!this.completions.filtered.length)
- return this.detach();
- this.openPopup(this.editor, prefix, keepPopupPosition);
- return;
- }
- this.gatherCompletions(this.editor, function(err, results) {
- var matches = results && results.matches;
- if (!matches || !matches.length)
- return this.detach();
- // TODO reenable this when we have proper change tracking
- // if (matches.length == 1)
- // return this.insertMatch(matches[0]);
- this.completions = new FilteredList(matches);
- this.completions.setFilter(results.prefix);
- if (!this.completions.filtered.length)
- return this.detach();
- this.openPopup(this.editor, results.prefix, keepPopupPosition);
- }.bind(this));
- };
- this.cancelContextMenu = function() {
- var stop = function(e) {
- this.editor.off("nativecontextmenu", stop);
- if (e && e.domEvent)
- event.stopEvent(e.domEvent);
- }.bind(this);
- setTimeout(stop, 10);
- this.editor.on("nativecontextmenu", stop);
- };
- }).call(Autocomplete.prototype);
- Autocomplete.startCommand = {
- name: "startAutocomplete",
- exec: function(editor) {
- if (!editor.completer)
- editor.completer = new Autocomplete();
- editor.completer.showPopup(editor);
- // needed for firefox on mac
- editor.completer.cancelContextMenu();
- },
- bindKey: "Ctrl-Space|Ctrl-Shift-Space|Alt-Space"
- };
- var FilteredList = function(array, filterText, mutateData) {
- this.all = array;
- this.filtered = array;
- this.filterText = filterText || "";
- };
- (function(){
- this.setFilter = function(str) {
- if (str.length > this.filterText && str.lastIndexOf(this.filterText, 0) === 0)
- var matches = this.filtered;
- else
- var matches = this.all;
- this.filterText = str;
- matches = this.filterCompletions(matches, this.filterText);
- matches = matches.sort(function(a, b) {
- return b.exactMatch - a.exactMatch || b.score - a.score;
- });
-
- // make unique
- var prev = null;
- matches = matches.filter(function(item){
- var caption = item.value || item.caption || item.snippet;
- if (caption === prev) return false;
- prev = caption;
- return true;
- });
-
- this.filtered = matches;
- };
- this.filterCompletions = function(items, needle) {
- var results = [];
- var upper = needle.toUpperCase();
- var lower = needle.toLowerCase();
- loop: for (var i = 0, item; item = items[i]; i++) {
- var caption = item.value || item.caption || item.snippet;
- if (!caption) continue;
- var lastIndex = -1;
- var matchMask = 0;
- var penalty = 0;
- var index, distance;
- // caption char iteration is faster in Chrome but slower in Firefox, so lets use indexOf
- for (var j = 0; j < needle.length; j++) {
- // TODO add penalty on case mismatch
- var i1 = caption.indexOf(lower[j], lastIndex + 1);
- var i2 = caption.indexOf(upper[j], lastIndex + 1);
- index = (i1 >= 0) ? ((i2 < 0 || i1 < i2) ? i1 : i2) : i2;
- if (index < 0)
- continue loop;
- distance = index - lastIndex - 1;
- if (distance > 0) {
- // first char mismatch should be more sensitive
- if (lastIndex === -1)
- penalty += 10;
- penalty += distance;
- }
- matchMask = matchMask | (1 << index);
- lastIndex = index;
- }
- item.matchMask = matchMask;
- item.exactMatch = penalty ? 0 : 1;
- item.score = (item.score || 0) - penalty;
- results.push(item);
- }
- return results;
- };
- }).call(FilteredList.prototype);
- exports.Autocomplete = Autocomplete;
- exports.FilteredList = FilteredList;
- });
|