diff --git a/demo/kitchen-sink/demo.js b/demo/kitchen-sink/demo.js index 4b0e1c32..81d7c024 100644 --- a/demo/kitchen-sink/demo.js +++ b/demo/kitchen-sink/demo.js @@ -116,20 +116,21 @@ env.editor.commands.addCommands([{ editor.gotoLine(line); }, readOnly: true -}/*, { - name: "find", - bindKey: {win: "Ctrl-F", mac: "Command-F"}, +}, { + name: "snippet", + bindKey: {win: "Alt-C", mac: "Command-Alt-C"}, exec: function(editor, needle) { - if (typeof needle == "object") { - var arg = this.name + " " + editor.getCopyText(); - editor.cmdLine.setValue(arg, 1); + if (typeof needle == "object") { + editor.cmdLine.setValue("snippet ", 1); editor.cmdLine.focus(); return; } - editor.find(needle); + var s = SnippetManager.getSnippetByName(needle, editor); + if (s) + SnippetManager.insertSnippet(editor, s.content); }, readOnly: true -}*/, { +}, { name: "focusCommandLine", bindKey: "shift-esc", exec: function(editor, needle) { editor.cmdLine.focus(); }, @@ -461,8 +462,8 @@ SnippetManager.register({ name: "testSnippet" }) jsSnippets.snippets = SnippetManager.parseSnippetFile(jsSnippets.snippetText) -SnippetManager.register(jsSnippets.snippets) - +SnippetManager.register(jsSnippets.snippets, "javascript") +window.SnippetManager = SnippetManager ace.commands.bindKey("Tab", function(editor) { var success = SnippetManager.expandWithTab(editor); diff --git a/lib/ace/snippets.js b/lib/ace/snippets.js index dee004ff..0879003f 100644 --- a/lib/ace/snippets.js +++ b/lib/ace/snippets.js @@ -30,10 +30,11 @@ define(function(require, exports, module) { "use strict"; -var Range = require("./range").Range var lang = require("./lib/lang") +var Range = require("./range").Range var HashHandler = require("./keyboard/hash_handler").HashHandler; var Tokenizer = require("./tokenizer").Tokenizer; +var comparePoints = Range.comparePoints; var SnippetManager = function() { this.snippetMap = {}; @@ -44,7 +45,7 @@ var SnippetManager = function() { this.getTokenizer = function() { function TabstopToken(str, _, stack) { str = str.substr(1); - if (/^\d+$/.test(str)) + if (/^\d+$/.test(str) && !stack.inFormatString) return [{tabstopId: parseInt(str, 10)}]; return [{text: str}] } @@ -53,23 +54,37 @@ var SnippetManager = function() { } SnippetManager.$tokenizer = new Tokenizer({ start: [ - {regex: /\\./, token: function(val, state, stack) { - if (stack.escapes == null) - stack.escapes = "`$\\"; - var ch = val[1]; - if ( - stack.escapes.indexOf(ch) == -1 || - (ch == "}" && stack.length) - ) { - val = ch; + {regex: /:/, onMatch: function(val, state, stack) { + if (stack.length && stack[0].expectIf) { + stack[0].expectIf = false; + stack[0].elseBranch = stack[0]; + return [stack[0]]; } + return ":"; + }}, + {regex: /\\./, onMatch: function(val, state, stack) { + var ch = val[1]; + if (ch == "}" && stack.length) { + val = ch; + }else if ("`$\\".indexOf(ch) != -1) { + val = ch; + } else if (stack.inFormatString) { + if (ch == "n") + val = "\n"; + else if (ch == "t") + val = "\n"; + else if ("ulULE".indexOf(ch) != -1) { + val = {changeCase: ch, local: ch > "a"}; + } + } + return [val]; }}, - {regex: /}/, token: function(val, state, stack) { + {regex: /}/, onMatch: function(val, state, stack) { return [stack.length ? stack.shift() : val]; }}, - {regex: /\$(?:\d+|\w+)/, token: TabstopToken}, - {regex: /\$\{[\dA-Z_a-z]+/, token: function(str, state, stack) { + {regex: /\$(?:\d+|\w+)/, onMatch: TabstopToken}, + {regex: /\$\{[\dA-Z_a-z]+/, onMatch: function(str, state, stack) { var t = TabstopToken(str.substr(1), state, stack); stack.unshift(t[0]); return t; @@ -77,28 +92,33 @@ var SnippetManager = function() { {regex: /\n/, token: "newline"} ], snippetVar: [ - {regex: "\\|" + escape("\\|") + "*\\|", token: function(val, state, stack) { + {regex: "\\|" + escape("\\|") + "*\\|", onMatch: function(val, state, stack) { stack[0].choices = val.slice(1, -1).split(","); }, next: "start"}, {regex: "/(" + escape("/") + "+)/(?:(" + escape("/") + "*)/)(\\w*):?", - token: function(val, state, stack) { + onMatch: function(val, state, stack) { val = this.splitRegex.exec(val); var ts = stack[0]; ts.guard = val[1]; ts.fmt = val[2]; - ts.flag = val[3] + ts.flag = val[3]; return ""; }, next: "start"}, - {regex: "`" + escape("`") + "*`", token: function(val, state, stack) { + {regex: "`" + escape("`") + "*`", onMatch: function(val, state, stack) { stack[0].code = val.splice(1, -1); return ""; }, next: "start"}, - {regex: ":[+-?]", token: "", next: "start"}, - {regex: ":|", token: "", next: "start"} + {regex: "\\?", onMatch: function(val, state, stack) { + if (stack[0]) + stack[0].expectIf = true; + }, next: "start"}, + {regex: "[^:}]*:?", token: "", next: "start"} ], formatString: [ - {regex: /\\[ulULEnt/]/, token: "escape"}, - {include: "$"} + {regex: "/(" + escape("/") + "+)/", token: "regex"}, + {regex: "", onMatch: function(val, state, stack) { + stack.inFormatString = true; + }, next: "start"} ] }); SnippetManager.prototype.getTokenizer = function() { @@ -107,15 +127,24 @@ var SnippetManager = function() { return SnippetManager.$tokenizer; }; - this.tokenizeTmSnippet = function(str) { - return this.getTokenizer().getLineTokens(str).tokens.map(function(x) { + this.tokenizeTmSnippet = function(str, startState) { + return this.getTokenizer().getLineTokens(str, startState).tokens.map(function(x) { return x.value || x; }); }; this.$getDefaultValue = function(editor, name) { + if (/^[A-Z]\d+$/.test(name)) { + var i = name.substr(1); + return (this.variables[name[0] + "__"] || {})[i]; + } + if (/^\d+$/.test(name)) { + return (this.variables.__ || {})[name]; + } name = name.replace(/^TM_/, ""); + if (!editor) + return; var s = editor.session; switch(name) { case "CURRENT_WORD": @@ -144,48 +173,88 @@ var SnippetManager = function() { this.variables = {}; this.getVariableValue = function(editor, varName) { if (this.variables.hasOwnProperty(varName)) - return this.variables[varName](editor, varName); - return this.$getDefaultValue(editor, varName); + return this.variables[varName](editor, varName) || ""; + return this.$getDefaultValue(editor, varName) || ""; }; // returns string formatted according to http://manual.macromates.com/en/regular_expressions#replacement_string_syntax_format_strings - this.tmStrFormat = function(str, regex, fmt, flags) { - var re = new RegExp(regex, flags.replace(/[^gi]/, "")); - var fmtTokens = this.getTokenizer().getLineTokens(fmt, "formatString"); - - return str.replace(re, function() { - var matches = arguments; - for (var i = 0; i < fmtParts; i++) { - + this.tmStrFormat = function(str, ch, editor) { + var flag = ch.flag || ""; + var re = ch.guard; + re = new RegExp(re, flag.replace(/[^gi]/, "")); + var fmtTokens = this.tokenizeTmSnippet(ch.fmt, "formatString"); + var _self = this; + var formatted = str.replace(re, function() { + _self.variables.__ = arguments; + var fmtParts = _self.resolveVariables(fmtTokens, editor); + var gChangeCase = "E"; + for (var i = 0; i < fmtParts.length; i++) { + var ch = fmtParts[i]; + if (typeof ch == "object") { + fmtParts[i] = ""; + if (ch.changeCase && ch.local) { + var next = fmtParts[i + 1]; + if (next && typeof next == "string") { + if (ch.changeCase == "u") + fmtParts[i] = next[0].toUpperCase(); + else + fmtParts[i] = next[0].toLowerCase(); + fmtParts[i + 1] = next.substr(1); + } + } else if (ch.changeCase) { + gChangeCase = ch.changeCase; + } + } else if (gChangeCase == "U") { + fmtParts[i] = ch.toUpperCase(); + } else if (gChangeCase == "L") { + fmtParts[i] = ch.toLowerCase(); + } } - return result; + return fmtParts.join(""); }); + this.variables.__ = null; + return formatted; }; this.resolveVariables = function(snippet, editor) { var result = []; for (var i = 0; i < snippet.length; i++) { - var ch = snippet[i] + var ch = snippet[i]; if (typeof ch == "string") { result.push(ch); - } else if (typeof ch != "object" || ch.processed) { + } else if (typeof ch != "object") { + continue; + } else if (ch.skip) { + gotoNext(ch); + } else if (ch.processed < i) { continue; } else if (ch.text) { var value = this.getVariableValue(editor, ch.text); - if (value) { - var i1 = snippet.indexOf(ch, i + 1); - if (i1 != -1) - i = i1; - if (ch.fmt) - value = this.tmStrFormat(value, ch.fmt); - result.push(value); + if (value && ch.fmt) + value = this.tmStrFormat(value, ch); + ch.processed = i; + if (ch.expectIf == null) { + if (value) { + result.push(value); + gotoNext(ch); + } } else { - ch.processed = true; + if (value) { + ch.skip = ch.elseBranch; + } else + gotoNext(ch); } } else if (ch.tabstopId != null) { result.push(ch); + } else if (ch.changeCase != null) { + result.push(ch); } } + function gotoNext(ch) { + var i1 = snippet.indexOf(ch, i + 1); + if (i1 != -1) + i = i1; + } return result; }; @@ -303,7 +372,12 @@ var SnippetManager = function() { cursor.column - snippet.replaceBefore.length, cursor.column + snippet.replaceAfter.length ); - this.insertSnippet(editor, snippet.content, snippet.matchBefore, snippet.matchAfter); + + this.variables.M__ = snippet.matchBefore; + this.variables.T__ = snippet.matchAfter; + this.insertSnippet(editor, snippet.content); + + this.variables.M__ = this.variables.T__ = null; return true; }; @@ -365,7 +439,7 @@ var SnippetManager = function() { if (s.tabTrigger && !s.trigger) { if (!s.guard && /^\w/.test(s.tabTrigger)) - s.guard = "\\\\b"; + s.guard = "\\b"; s.trigger = lang.escapeRegExp(s.tabTrigger); } @@ -414,6 +488,18 @@ var SnippetManager = function() { } return list; }; + this.getSnippetByName = function(name, editor) { + var scope = editor && this.$getScope(editor); + var snippetMap = this.snippetNameMap; + var snippet; + [scope, "_"].some(function(scope) { + var snippets = snippetMap[scope]; + if (snippets) + snippet = snippets[name]; + return !!snippet; + }, this); + return snippet; + }; }).call(SnippetManager.prototype); @@ -426,6 +512,7 @@ var TabstopManager = function(editor) { this.$onChange = this.onChange.bind(this); this.$onChangeSelection = this.onChangeSelection.bind(this); this.$onChangeSession = this.onChangeSession.bind(this); + this.$onAfterExec = this.onAfterExec.bind(this); this.attach(editor); }; (function() { @@ -439,6 +526,7 @@ var TabstopManager = function(editor) { this.editor.on("change", this.$onChange); this.editor.on("changeSelection", this.$onChangeSelection); this.editor.on("changeSession", this.$onChangeSession); + this.editor.commands.on("afterExec", this.$onAfterExec); this.editor.keyBinding.addKeyboardHandler(this.keyboardHandler); }; this.detach = function() { @@ -449,6 +537,7 @@ var TabstopManager = function(editor) { this.editor.removeListener("change", this.$onChange); this.editor.removeListener("changeSelection", this.$onChangeSelection); this.editor.removeListener("changeSession", this.$onChangeSession); + this.editor.commands.removeListener("afterExec", this.$onAfterExec); this.editor.keyBinding.removeKeyboardHandler(this.keyboardHandler); this.editor.tabstopManager = null; this.editor = null; @@ -456,51 +545,62 @@ var TabstopManager = function(editor) { this.onChange = function(e) { var changeRange = e.data.range; - var isInsert = e.data.action[0] == "i"; - if (isInsert) { - var start = changeRange.start; - var end = changeRange.end; - } else { - var end = changeRange.start; - var start = changeRange.end; - } + var isRemove = e.data.action[0] == "r"; + var start = changeRange.start; + var end = changeRange.end; var startRow = start.row; var endRow = end.row; var lineDif = endRow - startRow; - var colDiff = end.column - start.column; - var ranges = this.ranges; -console.log(e.data) + if (isRemove) { + lineDif = -lineDif; + colDiff = -colDiff; + } + if (!this.$inChange) { + var ts = this.selectedTabstop; + var changedOutside = !ts.some(function(r) { + return comparePoints(r.start, start) <= 0 && comparePoints(r.end, end) >= 0; + }); + if (changedOutside) + return this.detach(); + } + var ranges = this.ranges; for (var i = 0; i < ranges.length; i++) { var r = ranges[i]; - if (r.end.row < changeRange.start.row) + if (r.end.row < start.row) continue; -var b=r.clone() - if (Range.comparePoints(changeRange.start, r.start) < 0 - && Range.comparePoints(changeRange.end, r.end) > 0) { + + if (comparePoints(start, r.start) < 0 && comparePoints(end, r.end) > 0) { this.removeRange(r); - console.log(1) i--; continue; } - if (r.start.row == startRow && r.start.column > start.column) { + if (r.start.row == startRow && r.start.column > start.column) r.start.column += colDiff; - } - if (r.end.row == startRow && r.end.column >= start.column) { + if (r.end.row == startRow && r.end.column >= start.column) r.end.column += colDiff; - } - if (r.start.row >= startRow) { + if (r.start.row >= startRow) r.start.row += lineDif; - } - if (r.end.row >= startRow) { + if (r.end.row >= startRow) r.end.row += lineDif; - } - console.log(b+r+"") - if (Range.comparePoints(r.start, r.end) > 0) - this.removeRange(r); + + if (comparePoints(r.start, r.end) > 0) + this.removeRange(r); } + if (!ranges.length) + this.detach(); + }; + this.updateLinkedFields = function() { + var ts = this.selectedTabstop; + if (!ts.linkedRanges) + return; + + }; + this.onAfterExec = function(e) { + if (e.command && !e.command.readOnly) + this.updateLinkedFields(); }; this.onChangeSelection = function() { setTimeout(function() { @@ -611,7 +711,10 @@ var b=r.clone() }, "Esc": function(ed) { ed.tabstopManager.detach(); - } + }, + "Return": function(ed) { + ed.tabstopManager.tabNext(1); + }, }); }).call(TabstopManager.prototype); diff --git a/lib/ace/snippets/javascript.snippets b/lib/ace/snippets/javascript.snippets index b5c4f72c..0019314e 100644 --- a/lib/ace/snippets/javascript.snippets +++ b/lib/ace/snippets/javascript.snippets @@ -5,15 +5,15 @@ snippet proto }; # Function snippet fun - function ${1:function_name}(${2:argument}) { + function ${1?:function_name}(${2:argument}) { ${3:// body...} } # Anonymous Function -regex /(=)\s*|(:)\s*|(\()|\b/f/(\))?/ +regex /((=)\s*|(:)\s*|(\()|\b)/f/(\))?/ name f - function${M1||M2||M3? ${1:functionName $: }}($2) { + function${M1?: ${1:functionName}}($2) { ${0:$TM_SELECTED_TEXT} - }${M1?;}${M2?,}${M3?)} + }${M2?;}${M3?,}${M4?)} # Immediate function trigger \(?f\( endTrigger \)? @@ -165,7 +165,7 @@ snippet for- } # for (...) {...} snippet for - for (var ${1\n:i} = 0; $1 < ${2/\n/:Things}.length; $1${3/\n/:++}) { + for (var ${1:i} = 0; $1 < ${2:Things}.length; $1${3:++}) { ${0:$2[$1]} } # for (...) {...} (Improved Native For-Loop) @@ -173,26 +173,13 @@ snippet forr for (var ${1:i} = ${2:Things}.length - 1; $1 >= 0; $1${3:--}) { ${0:$2[$1]} } -snippet for- - -snippet for ?in - - -regex /^\s*/for\b\s*(\w+\b)?(\s*\d+\b)(\s*\d*)/ - 78789 -regex /^\s*/for (\w+) in (\w+)/ - ${include:for $1=$M1,$2=$M2}44 -regex /^\s*/for\b( \w+\b)?( \w+\b)(\.l?e?n?[ght]{0,3})/ - ${include:for $1=$M1,$2=$M2}44 -regex /^\s*/for (\w+) in (\w+)/ - ${include:for $1=$M1,$2=$M2}44 #modules snippet def define(function(require, exports, module) { "use strict"; - ${includeRepeated:Req} + var ${1/.*\///} = require("${1}"); $TM_SELECTED_TEXT }); @@ -202,5 +189,5 @@ guard ^\s* $0 snippet Req guard ^\s* - var ${1/.*\/(.)/\u$1/} = require("${1}").${1!0}; + var ${1/.*\/(.)/\u$1/} = require("${1}").${1/.*\/(.)/\u$1/}; $0 diff --git a/lib/ace/snippets_test.js b/lib/ace/snippets_test.js index 0c5e54c9..08be80ca 100644 --- a/lib/ace/snippets_test.js +++ b/lib/ace/snippets_test.js @@ -3,7 +3,7 @@ * * Copyright (c) 2010, 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 @@ -14,7 +14,7 @@ * * 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 @@ -41,10 +41,11 @@ var assert = require("./test/assertions"); module.exports = { "test: textmate style format strings" : function() { var fmt = SnippetManager.tmStrFormat; - assert.equal(fmt("abc", "/(.)(.)/$1/g"), "ac"); - assert.equal(fmt("abc", "/(.)(.)/$1(?2:Hello(1)2)/g"), "aHello(12)c2)"); - assert.equal(fmt("abc", "/(.)(.)/\\u$1+lL+\\u$2e|/g"), "A+lL+be|C+lL+e|"); - assert.equal(fmt("aBCD", "/(.)(.)/\\U$1+lL+\\l$2e/g"), "A+LL+be"); + SnippetManager.tmStrFormat("hello", { + guard: "(..)(.)(.)", + flag:"g", + fmt: "a\\UO\\l$1\\E$2" + }) == "aOHElo"; }, "test: parse snipmate file" : function() { var expected = [{ @@ -63,26 +64,26 @@ module.exports = { "name a\nregex /(?:(=)|(:))?\s*)/\\(?f/\\)/\n\t{$0}" + "\n\t\n\n#function\nsnippet f function\n\tfunction" ); - + assert.equal(JSON.stringify(expected), JSON.stringify(parsed)) }, - "test: parse snippet": function() { - var content = "-\\$$2a${1:x${$2:y$3}\\n\\}$TM_SELECTION}"; - var tokens = SnippetManager.tokenizeTmSnippet(content); - assert.equal(tokens.length, 14); - assert.equal(tokens[4] == tokens[13]); - assert.equal(tokens[2].tabstopId == 2); + "test: parse snippet": function() { + var content = "-\\$$2a${1:x${$2:y$3}\\n\\}$TM_SELECTION}"; + var tokens = SnippetManager.tokenizeTmSnippet(content); + assert.equal(tokens.length, 14); + assert.equal(tokens[4] == tokens[13]); + assert.equal(tokens[2].tabstopId == 2); - var content = "\\}${var/as\\/d/\\ul\\//g:s}" - var tokens = SnippetManager.tokenizeTmSnippet(content); - assert.equal(tokens.length, 4); - assert.equal(tokens[1], tokens[3]); - assert.equal(tokens[2], "s"); - assert.equal(tokens[1].text, "var"); - assert.equal(tokens[1].fmt, "\\ul\\/"); - assert.equal(tokens[1].guard, "as\\/d"); - assert.equal(tokens[1].flag, "g"); - } + var content = "\\}${var/as\\/d/\\ul\\//g:s}" + var tokens = SnippetManager.tokenizeTmSnippet(content); + assert.equal(tokens.length, 4); + assert.equal(tokens[1], tokens[3]); + assert.equal(tokens[2], "s"); + assert.equal(tokens[1].text, "var"); + assert.equal(tokens[1].fmt, "\\ul\\/"); + assert.equal(tokens[1].guard, "as\\/d"); + assert.equal(tokens[1].flag, "g"); + } }; });