diff --git a/demo/kitchen-sink/demo.js b/demo/kitchen-sink/demo.js index 1ace9cb0..634125f6 100644 --- a/demo/kitchen-sink/demo.js +++ b/demo/kitchen-sink/demo.js @@ -600,7 +600,7 @@ env.editSnippets = function() { editor.on("blur", function() { m.snippetText = editor.getValue(); snippetManager.unregister(m.snippets); - m.snippets = snippetManager.parseSnippetFile(m.snippetText); + m.snippets = snippetManager.parseSnippetFile(m.snippetText, m.scope); snippetManager.register(m.snippets); }) sp.$editors[0].once("changeMode", function() { diff --git a/lib/ace/autocomplete.js b/lib/ace/autocomplete.js index 16707595..9f7e6ad0 100644 --- a/lib/ace/autocomplete.js +++ b/lib/ace/autocomplete.js @@ -61,7 +61,7 @@ var Autocomplete = function() { }.bind(this)); }; - this.openPopup = function(editor, keepPopupPosition) { + this.openPopup = function(editor, prefix, keepPopupPosition) { if (!this.popup) this.$init(); @@ -70,7 +70,10 @@ var Autocomplete = function() { var renderer = editor.renderer; if (!keepPopupPosition) { var lineHeight = renderer.layerConfig.lineHeight; - var pos = renderer.$cursorLayer.getPixelPosition(null, true); + + 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; @@ -83,18 +86,24 @@ var Autocomplete = function() { this.detach = function() { this.editor.keyBinding.removeKeyboardHandler(this.keyboardHandler); - this.editor.removeEventListener("changeSelection", this.changeListener); - this.editor.removeEventListener("blur", this.changeListener); - this.editor.removeEventListener("mousedown", this.changeListener); + this.editor.off("changeSelection", this.changeListener); + this.editor.off("blur", this.changeListener); + this.editor.off("mousedown", this.changeListener); + 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 @@ -119,8 +128,8 @@ var Autocomplete = function() { var max = this.popup.session.getLength() - 1; switch(where) { - case "up": row = row <= 0 ? max : row - 1; break; - case "down": row = row >= max ? 0 : row + 1; break; + 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; } @@ -129,7 +138,6 @@ var Autocomplete = function() { }; this.insertMatch = function(data) { - this.detach(); if (!data) data = this.popup.getData(this.popup.getRow()); if (!data) @@ -138,15 +146,18 @@ var Autocomplete = function() { data.completer.insertMatch(this.editor); } else { if (this.completions.filterText) { - var range = this.editor.selection.getRange(); - range.start.column -= this.completions.filterText.length; - this.editor.session.remove(range); + 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.insert(data.value || data); + this.editor.execCommand("insertstring", data.value || data); } + this.detach(); }; this.commands = { @@ -161,52 +172,19 @@ var Autocomplete = function() { "Shift-Return": function(editor) { editor.completer.insertMatch(true); }, "Tab": function(editor) { editor.completer.insertMatch(); }, - "PageUp": function(editor) { editor.completer.popup.gotoPageDown(); }, - "PageDown": function(editor) { editor.completer.popup.gotoPageUp(); } - }; - - 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; - 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.score = (item.score || 0) - penalty; - results.push(item); - } - return results; + "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) { @@ -241,10 +219,22 @@ var Autocomplete = function() { 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) @@ -253,14 +243,11 @@ var Autocomplete = function() { // if (matches.length == 1) // return this.insertMatch(matches[0]); - - matches = this.filterCompletions(matches, results.prefix); - matches = matches.sort(function(a, b) { - return b.score - a.score; - }); this.completions = new FilteredList(matches); this.completions.setFilter(results.prefix); - this.openPopup(this.editor, keepPopupPosition); + if (!this.completions.filtered.length) + return this.detach(); + this.openPopup(this.editor, results.prefix, keepPopupPosition); }.bind(this)); }; @@ -288,16 +275,71 @@ Autocomplete.startCommand = { bindKey: "Ctrl-Space|Ctrl-Shift-Space|Alt-Space" }; -var FilteredList = function(array, mutateData) { +var FilteredList = function(array, filterText, mutateData) { this.all = array; - this.filtered = array.concat(); - this.filterText = ""; + this.filtered = array; + this.filterText = filterText || ""; }; (function(){ this.setFilter = function(str) { - this.filterText = 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; diff --git a/lib/ace/autocomplete/popup.js b/lib/ace/autocomplete/popup.js index 2d0bba1a..2475d6a5 100644 --- a/lib/ace/autocomplete/popup.js +++ b/lib/ace/autocomplete/popup.js @@ -77,7 +77,7 @@ var AcePopup = function(parentNode) { popup.renderer.$maxLines = 8; popup.renderer.$keepTextAreaAtCursor = false; - popup.setHighlightActiveLine(true); + popup.setHighlightActiveLine(false); // set default highlight color popup.session.highlight(""); popup.session.$searchHighlight.clazz = "ace_highlight-marker"; @@ -86,16 +86,42 @@ var AcePopup = function(parentNode) { var pos = e.getDocumentPosition(); popup.moveCursorToPosition(pos); popup.selection.clearSelection(); + selectionMarker.start.row = selectionMarker.end.row = pos.row; e.stop(); }); + var lastMouseEvent; var hoverMarker = new Range(-1,0,-1,Infinity); - hoverMarker.id = popup.session.addMarker(hoverMarker, "ace_line-hover", "fullLine"); + var selectionMarker = new Range(-1,0,-1,Infinity); + selectionMarker.id = popup.session.addMarker(selectionMarker, "ace_active-line", "fullLine"); + popup.setSelectOnHover = function(val) { + if (!val) { + hoverMarker.id = popup.session.addMarker(hoverMarker, "ace_line-hover", "fullLine"); + } else if (hoverMarker.id) { + popup.session.removeMarker(hoverMarker.id); + hoverMarker.id = null; + } + } + popup.setSelectOnHover(false) popup.on("mousemove", function(e) { - //if (popup.lastOpened) - var row = e.getDocumentPosition().row; - hoverMarker.start.row = hoverMarker.end.row = row; - popup.session._emit("changeBackMarker"); + lastMouseEvent = e; + lastMouseEvent.scrollTop = popup.renderer.scrollTop; + var row = lastMouseEvent.getDocumentPosition().row; + if (hoverMarker.start.row != row) { + popup.session._emit("changeBackMarker"); + if (!hoverMarker.id) + popup.setRow(row); + hoverMarker.start.row = hoverMarker.end.row = row; + } + }); + popup.renderer.on("beforeRender", function() { + if (lastMouseEvent && hoverMarker.start.row != -1) { + lastMouseEvent.$pos = null; + var row = lastMouseEvent.getDocumentPosition().row; + if (!hoverMarker.id) + popup.setRow(row); + hoverMarker.start.row = hoverMarker.end.row = row; + } }); var hideHoverMarker = function() { hoverMarker.start.row = hoverMarker.end.row = -1; @@ -104,12 +130,7 @@ var AcePopup = function(parentNode) { event.addListener(popup.container, "mouseout", hideHoverMarker); popup.on("hide", hideHoverMarker); popup.on("changeSelection", hideHoverMarker); - popup.on("mousewheel", function(e) { - setTimeout(function() { - popup._signal("mousemove", e); - }); - }); - + popup.session.doc.getLength = function() { return popup.data.length; }; @@ -137,7 +158,7 @@ var AcePopup = function(parentNode) { c = data.caption[i]; flag = data.matchMask & (1 << i) ? 1 : 0; if (last !== flag) { - tokens.push({type: data.className || "" + ( flag ? ".bold" : ""), value: c}); + tokens.push({type: data.className || "" + ( flag ? "completion-highlight" : ""), value: c}); last = flag; } else { tokens[tokens.length - 1].value += c; @@ -166,17 +187,15 @@ var AcePopup = function(parentNode) { popup.getData = function(row) { return popup.data[row]; }; - - popup.getRow = function() { - var line = this.getCursorPosition().row; - if (line == 0 && !this.getHighlightActiveLine()) - line = -1; - return line; + + popup.getRow = function() { + return selectionMarker.start.row; }; popup.setRow = function(line) { - popup.setHighlightActiveLine(line != -1); popup.selection.clearSelection(); - popup.moveCursorTo(line, 0 || 0); + selectionMarker.start.row = selectionMarker.end.row = line || 0; + popup.session._emit("changeBackMarker"); + popup.moveCursorTo(line || 0, 0); }; popup.hide = function() { @@ -200,13 +219,18 @@ var AcePopup = function(parentNode) { this._signal("show"); }; + + popup.getTextLeftOffset = function() { + return 1 + this.renderer.layerConfig.padding; + } return popup; }; dom.importCssString("\ .ace_autocomplete.ace-tm .ace_marker-layer .ace_active-line {\ - background-color: #abbffe;\ + background-color: #CAD6FA;\ + z-index: 1;\ }\ .ace_autocomplete.ace-tm .ace_line-hover {\ border: 1px solid #abbffe;\ @@ -223,13 +247,19 @@ dom.importCssString("\ text-align: right;\ z-index: -1;\ }\ +.ace_autocomplete .ace_completion-highlight{\ + color: #000;\ + text-shadow: 0 0 0.01px;\ +}\ .ace_autocomplete {\ - width: 200px;\ + width: 280px;\ z-index: 200000;\ - background: #f8f8f8;\ + background: #fbfbfb;\ + color: #444;\ border: 1px lightgray solid;\ position: fixed;\ box-shadow: 2px 3px 5px rgba(0,0,0,.2);\ + line-height: 1.4;\ }"); exports.AcePopup = AcePopup; diff --git a/lib/ace/css/editor.css b/lib/ace/css/editor.css index 3413f99c..acea6f89 100644 --- a/lib/ace/css/editor.css +++ b/lib/ace/css/editor.css @@ -274,7 +274,7 @@ max-width: 500px; padding: 4px; position: fixed; - z-index: 300; + z-index: 999999; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; diff --git a/lib/ace/edit_session.js b/lib/ace/edit_session.js index 3d3e95d2..c65e6818 100644 --- a/lib/ace/edit_session.js +++ b/lib/ace/edit_session.js @@ -992,7 +992,7 @@ var EditSession = function(text, mode) { * **/ this.setScrollTop = function(scrollTop) { - scrollTop = Math.round(scrollTop); + // TODO: should we force integer lineheight instead? scrollTop = Math.round(scrollTop); if (this.$scrollTop === scrollTop || isNaN(scrollTop)) return; @@ -1013,7 +1013,7 @@ var EditSession = function(text, mode) { * [Sets the value of the distance between the left of the editor and the leftmost part of the visible content.]{: #EditSession.setScrollLeft} **/ this.setScrollLeft = function(scrollLeft) { - scrollLeft = Math.round(scrollLeft); + // scrollLeft = Math.round(scrollLeft); if (this.$scrollLeft === scrollLeft || isNaN(scrollLeft)) return; diff --git a/lib/ace/ext/language_tools.js b/lib/ace/ext/language_tools.js index 9c4c20dc..e5cd8bb4 100644 --- a/lib/ace/ext/language_tools.js +++ b/lib/ace/ext/language_tools.js @@ -53,10 +53,13 @@ var snippetCompleter = { var snippets = snippetMap[scope] || []; for (var i = snippets.length; i--;) { var s = snippets[i]; + var caption = s.name || s.tabTrigger; + if (!caption) + continue; completions.push({ - caption: s.tabTrigger, + caption: caption, snippet: s.content, - meta: "snippet" + meta: s.tabTrigger && !s.name ? s.tabTrigger + "\u21E5 " : "snippet" }); } }, this); diff --git a/lib/ace/multi_select.js b/lib/ace/multi_select.js index 324b1982..0612357d 100644 --- a/lib/ace/multi_select.js +++ b/lib/ace/multi_select.js @@ -196,7 +196,7 @@ var EditSession = require("./edit_session").EditSession; * @method Selection.getAllRanges **/ this.getAllRanges = function() { - return this.rangeList.ranges.concat(); + return this.rangeCount ? this.rangeList.ranges.concat() : [this.getRange()]; }; /** diff --git a/lib/ace/snippets.js b/lib/ace/snippets.js index 6e11a932..aa1d71b5 100644 --- a/lib/ace/snippets.js +++ b/lib/ace/snippets.js @@ -282,38 +282,73 @@ var SnippetManager = function() { if (typeof p != "object") return; var id = p.tabstopId; - if (!tabstops[id]) { - tabstops[id] = []; - tabstops[id].index = id; - tabstops[id].value = ""; + var ts = tabstops[id]; + if (!ts) { + ts = tabstops[id] = []; + ts.index = id; + ts.value = ""; } - if (tabstops[id].indexOf(p) != -1) + if (ts.indexOf(p) !== -1) return; - tabstops[id].push(p); + ts.push(p); var i1 = tokens.indexOf(p, i + 1); - if (i1 == -1) + if (i1 === -1) return; - var value = tokens.slice(i + 1, i1).join(""); - if (value) - tabstops[id].value = value; + + var value = tokens.slice(i + 1, i1); + var isNested = value.some(function(t) {return typeof t === "object"}); + if (isNested && !ts.value) { + ts.value = value; + } else if (value.length && (!ts.value || typeof ts.value !== "string")) { + ts.value = value.join(""); + } }); - tabstops.forEach(function(ts) { - ts.value && ts.forEach(function(p) { - var i = tokens.indexOf(p); - var i1 = tokens.indexOf(p, i + 1); - if (i1 == -1) - tokens.splice(i + 1, 0, ts.value, p); - else if (i1 == i + 1) - tokens.splice(i + 1, 0, ts.value); - }); - }); + // expand tabstop values + tabstops.forEach(function(ts) {ts.length = 0}); + var expanding = {}; + function copyValue(val) { + var copy = [] + for (var i = 0; i < val.length; i++) { + var p = val[i]; + if (typeof p == "object") { + if (expanding[p.tabstopId]) + continue; + var j = val.lastIndexOf(p, i - 1); + p = copy[j] || {tabstopId: p.tabstopId}; + } + copy[i] = p; + } + return copy; + } + for (var i = 0; i < tokens.length; i++) { + var p = tokens[i]; + if (typeof p != "object") + continue; + var id = p.tabstopId; + var i1 = tokens.indexOf(p, i + 1); + if (expanding[id] == p) { + expanding[id] = null; + continue; + } + + var ts = tabstops[id]; + var arg = typeof ts.value == "string" ? [ts.value] : copyValue(ts.value); + arg.unshift(i + 1, Math.max(0, i1 - i)); + arg.push(p); + expanding[id] = p; + tokens.splice.apply(tokens, arg); + + if (ts.indexOf(p) === -1) + ts.push(p); + }; + // convert to plain text var row = 0, column = 0; var text = ""; tokens.forEach(function(t) { - if (typeof t == "string") { - if (t[0] == "\n"){ + if (typeof t === "string") { + if (t[0] === "\n"){ column = t.length - 1; row ++; } else @@ -484,7 +519,7 @@ var SnippetManager = function() { snippets.forEach(removeSnippet); }; this.parseSnippetFile = function(str) { - str = str.replace(/\r/, ""); + str = str.replace(/\r/g, ""); var list = [], snippet = {}; var re = /^#.*|^({[\s\S]*})\s*$|^(\S+) (.*)$|^((?:\n*\t.*)+)/gm; var m; diff --git a/lib/ace/snippets/php.snippets b/lib/ace/snippets/php.snippets index 97f17bc1..8d11a281 100644 --- a/lib/ace/snippets/php.snippets +++ b/lib/ace/snippets/php.snippets @@ -16,12 +16,12 @@ snippet use use ${1:Foo\Bar\Baz}; ${2} snippet c - ${1:abstract }class ${2:`Filename()`} + ${1:abstract }class ${2:$FILENAME} { ${3} } snippet i - interface ${1:`Filename()`} + interface ${1:$FILENAME} { ${2} } @@ -45,7 +45,7 @@ snippet sm * * @param ${2:$1} $$1 ${3:description} * - * @return ${4:`Filename()`} + * @return ${4:$FILENAME} */ ${5:public} function set${6:$2}(${7:$2 }$$1) { @@ -202,7 +202,7 @@ snippet interface * @package ${3:default} * @author ${4:`g:snips_author`} */ - interface ${1:`Filename()`} + interface ${1:$FILENAME} { ${5} } @@ -211,7 +211,7 @@ snippet class /** * ${1} */ - class ${2:`Filename()`} + class ${2:$FILENAME} { ${3} /** @@ -345,7 +345,7 @@ snippet gs * * @param $2 $$1 ${5:description} * - * @return ${6:`Filename()`} + * @return ${6:$FILENAME} */ public function set$3(${7:$2 }$$1) { diff --git a/lib/ace/snippets_test.js b/lib/ace/snippets_test.js index 52aab4f0..1870d950 100644 --- a/lib/ace/snippets_test.js +++ b/lib/ace/snippets_test.js @@ -34,11 +34,20 @@ if (typeof process !== "undefined") { define(function(require, exports, module) { "use strict"; +var EditSession = require("./edit_session").EditSession; +var Editor = require("./editor").Editor; +var MockRenderer = require("./test/mockrenderer").MockRenderer; +var MultiSelect = require("./multi_select").MultiSelect; var snippetManager = require("./snippets").snippetManager; var assert = require("./test/assertions"); module.exports = { + setUp : function(next) { + this.editor = new Editor(new MockRenderer()); + next(); + }, + "test: textmate style format strings" : function() { var fmt = snippetManager.tmStrFormat; snippetManager.tmStrFormat("hello", { @@ -86,6 +95,32 @@ module.exports = { assert.equal(tokens[1].fmt, "\\ul\\/"); assert.equal(tokens[1].guard, "as\\/d"); assert.equal(tokens[1].flag, "g"); + }, + "test: expand snippet with nested tabstops": function() { + var content = "-${1}-${1:1}--${2:2 ${3} 2}-${3:3 $1 3}-${4:4 $2 4}"; + this.editor.setValue(""); + snippetManager.insertSnippet(this.editor, content); + assert.equal(this.editor.getValue(), "-1-1--2 3 1 3 2-3 1 3-4 2 3 1 3 2 4"); + + assert.equal(this.editor.getSelectedText(), "1\n1\n1\n1\n1"); + this.editor.tabstopManager.tabNext(); + assert.equal(this.editor.getSelectedText(), "2 3 1 3 2\n2 3 1 3 2"); + this.editor.tabstopManager.tabNext(); + assert.equal(this.editor.getSelectedText(), "3 1 3\n3 1 3\n3 1 3"); + this.editor.tabstopManager.tabNext(); + assert.equal(this.editor.getSelectedText(), "4 2 3 1 3 2 4"); + this.editor.tabstopManager.tabNext(); + assert.equal(this.editor.getSelectedText(), ""); + + this.editor.setValue(""); + snippetManager.insertSnippet(this.editor, "-${1:a$2}-${2:b$1}"); + assert.equal(this.editor.getValue(), "-ab-ba"); + + assert.equal(this.editor.getSelectedText(), "ab\na"); + this.editor.tabstopManager.tabNext(); + assert.equal(this.editor.getSelectedText(), "b\nba"); + this.editor.tabstopManager.tabNext(); + assert.equal(this.editor.getSelectedText(), ""); } };