emmet.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. /* ***** BEGIN LICENSE BLOCK *****
  2. * Distributed under the BSD license:
  3. *
  4. * Copyright (c) 2010, Ajax.org B.V.
  5. * All rights reserved.
  6. *
  7. * Redistribution and use in source and binary forms, with or without
  8. * modification, are permitted provided that the following conditions are met:
  9. * * Redistributions of source code must retain the above copyright
  10. * notice, this list of conditions and the following disclaimer.
  11. * * Redistributions in binary form must reproduce the above copyright
  12. * notice, this list of conditions and the following disclaimer in the
  13. * documentation and/or other materials provided with the distribution.
  14. * * Neither the name of Ajax.org B.V. nor the
  15. * names of its contributors may be used to endorse or promote products
  16. * derived from this software without specific prior written permission.
  17. *
  18. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  19. * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  20. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  21. * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
  22. * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  23. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  24. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  25. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  26. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  27. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  28. *
  29. * ***** END LICENSE BLOCK ***** */
  30. define(function(require, exports, module) {
  31. "use strict";
  32. var HashHandler = require("ace/keyboard/hash_handler").HashHandler;
  33. var Editor = require("ace/editor").Editor;
  34. var snippetManager = require("ace/snippets").snippetManager;
  35. var Range = require("ace/range").Range;
  36. var emmet, emmetPath;
  37. /**
  38. * Implementation of {@link IEmmetEditor} interface for Ace
  39. */
  40. function AceEmmetEditor() {}
  41. AceEmmetEditor.prototype = {
  42. setupContext: function(editor) {
  43. this.ace = editor;
  44. this.indentation = editor.session.getTabString();
  45. if (!emmet)
  46. emmet = window.emmet;
  47. emmet.require("resources").setVariable("indentation", this.indentation);
  48. this.$syntax = null;
  49. this.$syntax = this.getSyntax();
  50. },
  51. /**
  52. * Returns character indexes of selected text: object with <code>start</code>
  53. * and <code>end</code> properties. If there's no selection, should return
  54. * object with <code>start</code> and <code>end</code> properties referring
  55. * to current caret position
  56. * @return {Object}
  57. * @example
  58. * var selection = editor.getSelectionRange();
  59. * alert(selection.start + ', ' + selection.end);
  60. */
  61. getSelectionRange: function() {
  62. // TODO should start be caret position instead?
  63. var range = this.ace.getSelectionRange();
  64. var doc = this.ace.session.doc;
  65. return {
  66. start: doc.positionToIndex(range.start),
  67. end: doc.positionToIndex(range.end)
  68. };
  69. },
  70. /**
  71. * Creates selection from <code>start</code> to <code>end</code> character
  72. * indexes. If <code>end</code> is ommited, this method should place caret
  73. * and <code>start</code> index
  74. * @param {Number} start
  75. * @param {Number} [end]
  76. * @example
  77. * editor.createSelection(10, 40);
  78. *
  79. * //move caret to 15th character
  80. * editor.createSelection(15);
  81. */
  82. createSelection: function(start, end) {
  83. var doc = this.ace.session.doc;
  84. this.ace.selection.setRange({
  85. start: doc.indexToPosition(start),
  86. end: doc.indexToPosition(end)
  87. });
  88. },
  89. /**
  90. * Returns current line's start and end indexes as object with <code>start</code>
  91. * and <code>end</code> properties
  92. * @return {Object}
  93. * @example
  94. * var range = editor.getCurrentLineRange();
  95. * alert(range.start + ', ' + range.end);
  96. */
  97. getCurrentLineRange: function() {
  98. var ace = this.ace;
  99. var row = ace.getCursorPosition().row;
  100. var lineLength = ace.session.getLine(row).length;
  101. var index = ace.session.doc.positionToIndex({row: row, column: 0});
  102. return {
  103. start: index,
  104. end: index + lineLength
  105. };
  106. },
  107. /**
  108. * Returns current caret position
  109. * @return {Number|null}
  110. */
  111. getCaretPos: function(){
  112. var pos = this.ace.getCursorPosition();
  113. return this.ace.session.doc.positionToIndex(pos);
  114. },
  115. /**
  116. * Set new caret position
  117. * @param {Number} index Caret position
  118. */
  119. setCaretPos: function(index){
  120. var pos = this.ace.session.doc.indexToPosition(index);
  121. this.ace.selection.moveToPosition(pos);
  122. },
  123. /**
  124. * Returns content of current line
  125. * @return {String}
  126. */
  127. getCurrentLine: function() {
  128. var row = this.ace.getCursorPosition().row;
  129. return this.ace.session.getLine(row);
  130. },
  131. /**
  132. * Replace editor's content or it's part (from <code>start</code> to
  133. * <code>end</code> index). If <code>value</code> contains
  134. * <code>caret_placeholder</code>, the editor will put caret into
  135. * this position. If you skip <code>start</code> and <code>end</code>
  136. * arguments, the whole target's content will be replaced with
  137. * <code>value</code>.
  138. *
  139. * If you pass <code>start</code> argument only,
  140. * the <code>value</code> will be placed at <code>start</code> string
  141. * index of current content.
  142. *
  143. * If you pass <code>start</code> and <code>end</code> arguments,
  144. * the corresponding substring of current target's content will be
  145. * replaced with <code>value</code>.
  146. * @param {String} value Content you want to paste
  147. * @param {Number} [start] Start index of editor's content
  148. * @param {Number} [end] End index of editor's content
  149. * @param {Boolean} [noIndent] Do not auto indent <code>value</code>
  150. */
  151. replaceContent: function(value, start, end, noIndent) {
  152. if (end == null)
  153. end = start == null ? this.getContent().length : start;
  154. if (start == null)
  155. start = 0;
  156. var editor = this.ace;
  157. var doc = editor.session.doc;
  158. var range = Range.fromPoints(doc.indexToPosition(start), doc.indexToPosition(end));
  159. editor.session.remove(range);
  160. range.end = range.start;
  161. //editor.selection.setRange(range);
  162. value = this.$updateTabstops(value);
  163. snippetManager.insertSnippet(editor, value);
  164. },
  165. /**
  166. * Returns editor's content
  167. * @return {String}
  168. */
  169. getContent: function(){
  170. return this.ace.getValue();
  171. },
  172. /**
  173. * Returns current editor's syntax mode
  174. * @return {String}
  175. */
  176. getSyntax: function() {
  177. if (this.$syntax)
  178. return this.$syntax;
  179. var syntax = this.ace.session.$modeId.split("/").pop();
  180. if (syntax == "html" || syntax == "php") {
  181. var cursor = this.ace.getCursorPosition();
  182. var state = this.ace.session.getState(cursor.row);
  183. if (typeof state != "string")
  184. state = state[0];
  185. if (state) {
  186. state = state.split("-");
  187. if (state.length > 1)
  188. syntax = state[0];
  189. else if (syntax == "php")
  190. syntax = "html";
  191. }
  192. }
  193. return syntax;
  194. },
  195. /**
  196. * Returns current output profile name (@see emmet#setupProfile)
  197. * @return {String}
  198. */
  199. getProfileName: function() {
  200. switch (this.getSyntax()) {
  201. case "css": return "css";
  202. case "xml":
  203. case "xsl":
  204. return "xml";
  205. case "html":
  206. var profile = emmet.require("resources").getVariable("profile");
  207. // no forced profile, guess from content html or xhtml?
  208. if (!profile)
  209. profile = this.ace.session.getLines(0,2).join("").search(/<!DOCTYPE[^>]+XHTML/i) != -1 ? "xhtml": "html";
  210. return profile;
  211. default:
  212. var mode = this.ace.session.$mode;
  213. return mode.emmetConfig && mode.emmetConfig.profile || "xhtml";
  214. }
  215. },
  216. /**
  217. * Ask user to enter something
  218. * @param {String} title Dialog title
  219. * @return {String} Entered data
  220. * @since 0.65
  221. */
  222. prompt: function(title) {
  223. return prompt(title);
  224. },
  225. /**
  226. * Returns current selection
  227. * @return {String}
  228. * @since 0.65
  229. */
  230. getSelection: function() {
  231. return this.ace.session.getTextRange();
  232. },
  233. /**
  234. * Returns current editor's file path
  235. * @return {String}
  236. * @since 0.65
  237. */
  238. getFilePath: function() {
  239. return "";
  240. },
  241. // update tabstops: make sure all caret placeholders are unique
  242. // by default, abbreviation parser generates all unlinked (un-mirrored)
  243. // tabstops as ${0}, so we have upgrade all caret tabstops with unique
  244. // positions but make sure that all other tabstops are not linked accidentally
  245. // based on https://github.com/sergeche/emmet-sublime/blob/master/editor.js#L119-L171
  246. $updateTabstops: function(value) {
  247. var base = 1000;
  248. var zeroBase = 0;
  249. var lastZero = null;
  250. var range = emmet.require('range');
  251. var ts = emmet.require('tabStops');
  252. var settings = emmet.require('resources').getVocabulary("user");
  253. var tabstopOptions = {
  254. tabstop: function(data) {
  255. var group = parseInt(data.group, 10);
  256. var isZero = group === 0;
  257. if (isZero)
  258. group = ++zeroBase;
  259. else
  260. group += base;
  261. var placeholder = data.placeholder;
  262. if (placeholder) {
  263. // recursively update nested tabstops
  264. placeholder = ts.processText(placeholder, tabstopOptions);
  265. }
  266. var result = '${' + group + (placeholder ? ':' + placeholder : '') + '}';
  267. if (isZero) {
  268. lastZero = range.create(data.start, result);
  269. }
  270. return result;
  271. },
  272. escape: function(ch) {
  273. if (ch == '$') return '\\$';
  274. if (ch == '\\') return '\\\\';
  275. return ch;
  276. }
  277. };
  278. value = ts.processText(value, tabstopOptions);
  279. if (settings.variables['insert_final_tabstop'] && !/\$\{0\}$/.test(value)) {
  280. value += '${0}';
  281. } else if (lastZero) {
  282. value = emmet.require('utils').replaceSubstring(value, '${0}', lastZero);
  283. }
  284. return value;
  285. }
  286. };
  287. var keymap = {
  288. expand_abbreviation: {"mac": "ctrl+alt+e", "win": "alt+e"},
  289. match_pair_outward: {"mac": "ctrl+d", "win": "ctrl+,"},
  290. match_pair_inward: {"mac": "ctrl+j", "win": "ctrl+shift+0"},
  291. matching_pair: {"mac": "ctrl+alt+j", "win": "alt+j"},
  292. next_edit_point: "alt+right",
  293. prev_edit_point: "alt+left",
  294. toggle_comment: {"mac": "command+/", "win": "ctrl+/"},
  295. split_join_tag: {"mac": "shift+command+'", "win": "shift+ctrl+`"},
  296. remove_tag: {"mac": "command+'", "win": "shift+ctrl+;"},
  297. evaluate_math_expression: {"mac": "shift+command+y", "win": "shift+ctrl+y"},
  298. increment_number_by_1: "ctrl+up",
  299. decrement_number_by_1: "ctrl+down",
  300. increment_number_by_01: "alt+up",
  301. decrement_number_by_01: "alt+down",
  302. increment_number_by_10: {"mac": "alt+command+up", "win": "shift+alt+up"},
  303. decrement_number_by_10: {"mac": "alt+command+down", "win": "shift+alt+down"},
  304. select_next_item: {"mac": "shift+command+.", "win": "shift+ctrl+."},
  305. select_previous_item: {"mac": "shift+command+,", "win": "shift+ctrl+,"},
  306. reflect_css_value: {"mac": "shift+command+r", "win": "shift+ctrl+r"},
  307. encode_decode_data_url: {"mac": "shift+ctrl+d", "win": "ctrl+'"},
  308. // update_image_size: {"mac": "shift+ctrl+i", "win": "ctrl+u"},
  309. // expand_as_you_type: "ctrl+alt+enter",
  310. // wrap_as_you_type: {"mac": "shift+ctrl+g", "win": "shift+ctrl+g"},
  311. expand_abbreviation_with_tab: "Tab",
  312. wrap_with_abbreviation: {"mac": "shift+ctrl+a", "win": "shift+ctrl+a"}
  313. };
  314. var editorProxy = new AceEmmetEditor();
  315. exports.commands = new HashHandler();
  316. exports.runEmmetCommand = function runEmmetCommand(editor) {
  317. try {
  318. editorProxy.setupContext(editor);
  319. var actions = emmet.require("actions");
  320. if (this.action == "expand_abbreviation_with_tab") {
  321. if (!editor.selection.isEmpty())
  322. return false;
  323. }
  324. if (this.action == "wrap_with_abbreviation") {
  325. // without setTimeout prompt doesn't work on firefox
  326. return setTimeout(function() {
  327. actions.run("wrap_with_abbreviation", editorProxy);
  328. }, 0);
  329. }
  330. var pos = editor.selection.lead;
  331. var token = editor.session.getTokenAt(pos.row, pos.column);
  332. if (token && /\btag\b/.test(token.type))
  333. return false;
  334. var result = actions.run(this.action, editorProxy);
  335. } catch(e) {
  336. if (!emmet) {
  337. load(runEmmetCommand.bind(this, editor));
  338. return true;
  339. }
  340. editor._signal("changeStatus", typeof e == "string" ? e : e.message);
  341. console.log(e);
  342. result = false;
  343. }
  344. return result;
  345. };
  346. for (var command in keymap) {
  347. exports.commands.addCommand({
  348. name: "emmet:" + command,
  349. action: command,
  350. bindKey: keymap[command],
  351. exec: exports.runEmmetCommand,
  352. multiSelectAction: "forEach"
  353. });
  354. }
  355. exports.updateCommands = function(editor, enabled) {
  356. if (enabled) {
  357. editor.keyBinding.addKeyboardHandler(exports.commands);
  358. } else {
  359. editor.keyBinding.removeKeyboardHandler(exports.commands);
  360. }
  361. };
  362. exports.isSupportedMode = function(mode) {
  363. if (!mode) return false;
  364. if (mode.emmetConfig) return true;
  365. var id = mode.$id || mode;
  366. return /css|less|scss|sass|stylus|html|php|twig|ejs|handlebars/.test(id);
  367. };
  368. exports.isAvailable = function(editor, command) {
  369. if (/(evaluate_math_expression|expand_abbreviation)$/.test(command))
  370. return true;
  371. var mode = editor.session.$mode;
  372. var isSupported = exports.isSupportedMode(mode);
  373. if (isSupported && mode.$modes) {
  374. // TODO refactor mode delegates to make this simpler
  375. try {
  376. editorProxy.setupContext(editor);
  377. if (/js|php/.test(editorProxy.getSyntax()))
  378. isSupported = false;
  379. } catch(e) {}
  380. }
  381. return isSupported;
  382. }
  383. var onChangeMode = function(e, target) {
  384. var editor = target;
  385. if (!editor)
  386. return;
  387. var enabled = exports.isSupportedMode(editor.session.$mode);
  388. if (e.enableEmmet === false)
  389. enabled = false;
  390. if (enabled)
  391. load();
  392. exports.updateCommands(editor, enabled);
  393. };
  394. var load = function(cb) {
  395. if (typeof emmetPath == "string") {
  396. require("ace/config").loadModule(emmetPath, function() {
  397. emmetPath = null;
  398. cb && cb();
  399. });
  400. }
  401. };
  402. exports.AceEmmetEditor = AceEmmetEditor;
  403. require("ace/config").defineOptions(Editor.prototype, "editor", {
  404. enableEmmet: {
  405. set: function(val) {
  406. this[val ? "on" : "removeListener"]("changeMode", onChangeMode);
  407. onChangeMode({enableEmmet: !!val}, this);
  408. },
  409. value: true
  410. }
  411. });
  412. exports.setCore = function(e) {
  413. if (typeof e == "string")
  414. emmetPath = e;
  415. else
  416. emmet = e;
  417. };
  418. });