| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461 |
- /* ***** BEGIN LICENSE BLOCK *****
- * Distributed under the BSD license:
- *
- * 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
- * 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("ace/keyboard/hash_handler").HashHandler;
- var Editor = require("ace/editor").Editor;
- var snippetManager = require("ace/snippets").snippetManager;
- var Range = require("ace/range").Range;
- var emmet, emmetPath;
- /**
- * Implementation of {@link IEmmetEditor} interface for Ace
- */
- function AceEmmetEditor() {}
- AceEmmetEditor.prototype = {
- setupContext: function(editor) {
- this.ace = editor;
- this.indentation = editor.session.getTabString();
- if (!emmet)
- emmet = window.emmet;
- emmet.require("resources").setVariable("indentation", this.indentation);
- this.$syntax = null;
- this.$syntax = this.getSyntax();
- },
- /**
- * Returns character indexes of selected text: object with <code>start</code>
- * and <code>end</code> properties. If there's no selection, should return
- * object with <code>start</code> and <code>end</code> properties referring
- * to current caret position
- * @return {Object}
- * @example
- * var selection = editor.getSelectionRange();
- * alert(selection.start + ', ' + selection.end);
- */
- getSelectionRange: function() {
- // TODO should start be caret position instead?
- var range = this.ace.getSelectionRange();
- var doc = this.ace.session.doc;
- return {
- start: doc.positionToIndex(range.start),
- end: doc.positionToIndex(range.end)
- };
- },
- /**
- * Creates selection from <code>start</code> to <code>end</code> character
- * indexes. If <code>end</code> is ommited, this method should place caret
- * and <code>start</code> index
- * @param {Number} start
- * @param {Number} [end]
- * @example
- * editor.createSelection(10, 40);
- *
- * //move caret to 15th character
- * editor.createSelection(15);
- */
- createSelection: function(start, end) {
- var doc = this.ace.session.doc;
- this.ace.selection.setRange({
- start: doc.indexToPosition(start),
- end: doc.indexToPosition(end)
- });
- },
- /**
- * Returns current line's start and end indexes as object with <code>start</code>
- * and <code>end</code> properties
- * @return {Object}
- * @example
- * var range = editor.getCurrentLineRange();
- * alert(range.start + ', ' + range.end);
- */
- getCurrentLineRange: function() {
- var ace = this.ace;
- var row = ace.getCursorPosition().row;
- var lineLength = ace.session.getLine(row).length;
- var index = ace.session.doc.positionToIndex({row: row, column: 0});
- return {
- start: index,
- end: index + lineLength
- };
- },
- /**
- * Returns current caret position
- * @return {Number|null}
- */
- getCaretPos: function(){
- var pos = this.ace.getCursorPosition();
- return this.ace.session.doc.positionToIndex(pos);
- },
- /**
- * Set new caret position
- * @param {Number} index Caret position
- */
- setCaretPos: function(index){
- var pos = this.ace.session.doc.indexToPosition(index);
- this.ace.selection.moveToPosition(pos);
- },
- /**
- * Returns content of current line
- * @return {String}
- */
- getCurrentLine: function() {
- var row = this.ace.getCursorPosition().row;
- return this.ace.session.getLine(row);
- },
- /**
- * Replace editor's content or it's part (from <code>start</code> to
- * <code>end</code> index). If <code>value</code> contains
- * <code>caret_placeholder</code>, the editor will put caret into
- * this position. If you skip <code>start</code> and <code>end</code>
- * arguments, the whole target's content will be replaced with
- * <code>value</code>.
- *
- * If you pass <code>start</code> argument only,
- * the <code>value</code> will be placed at <code>start</code> string
- * index of current content.
- *
- * If you pass <code>start</code> and <code>end</code> arguments,
- * the corresponding substring of current target's content will be
- * replaced with <code>value</code>.
- * @param {String} value Content you want to paste
- * @param {Number} [start] Start index of editor's content
- * @param {Number} [end] End index of editor's content
- * @param {Boolean} [noIndent] Do not auto indent <code>value</code>
- */
- replaceContent: function(value, start, end, noIndent) {
- if (end == null)
- end = start == null ? this.getContent().length : start;
- if (start == null)
- start = 0;
-
- var editor = this.ace;
- var doc = editor.session.doc;
- var range = Range.fromPoints(doc.indexToPosition(start), doc.indexToPosition(end));
- editor.session.remove(range);
-
- range.end = range.start;
- //editor.selection.setRange(range);
-
- value = this.$updateTabstops(value);
- snippetManager.insertSnippet(editor, value);
- },
- /**
- * Returns editor's content
- * @return {String}
- */
- getContent: function(){
- return this.ace.getValue();
- },
- /**
- * Returns current editor's syntax mode
- * @return {String}
- */
- getSyntax: function() {
- if (this.$syntax)
- return this.$syntax;
- var syntax = this.ace.session.$modeId.split("/").pop();
- if (syntax == "html" || syntax == "php") {
- var cursor = this.ace.getCursorPosition();
- var state = this.ace.session.getState(cursor.row);
- if (typeof state != "string")
- state = state[0];
- if (state) {
- state = state.split("-");
- if (state.length > 1)
- syntax = state[0];
- else if (syntax == "php")
- syntax = "html";
- }
- }
- return syntax;
- },
- /**
- * Returns current output profile name (@see emmet#setupProfile)
- * @return {String}
- */
- getProfileName: function() {
- switch (this.getSyntax()) {
- case "css": return "css";
- case "xml":
- case "xsl":
- return "xml";
- case "html":
- var profile = emmet.require("resources").getVariable("profile");
- // no forced profile, guess from content html or xhtml?
- if (!profile)
- profile = this.ace.session.getLines(0,2).join("").search(/<!DOCTYPE[^>]+XHTML/i) != -1 ? "xhtml": "html";
- return profile;
- default:
- var mode = this.ace.session.$mode;
- return mode.emmetConfig && mode.emmetConfig.profile || "xhtml";
- }
- },
- /**
- * Ask user to enter something
- * @param {String} title Dialog title
- * @return {String} Entered data
- * @since 0.65
- */
- prompt: function(title) {
- return prompt(title);
- },
- /**
- * Returns current selection
- * @return {String}
- * @since 0.65
- */
- getSelection: function() {
- return this.ace.session.getTextRange();
- },
- /**
- * Returns current editor's file path
- * @return {String}
- * @since 0.65
- */
- getFilePath: function() {
- return "";
- },
-
- // update tabstops: make sure all caret placeholders are unique
- // by default, abbreviation parser generates all unlinked (un-mirrored)
- // tabstops as ${0}, so we have upgrade all caret tabstops with unique
- // positions but make sure that all other tabstops are not linked accidentally
- // based on https://github.com/sergeche/emmet-sublime/blob/master/editor.js#L119-L171
- $updateTabstops: function(value) {
- var base = 1000;
- var zeroBase = 0;
- var lastZero = null;
- var range = emmet.require('range');
- var ts = emmet.require('tabStops');
- var settings = emmet.require('resources').getVocabulary("user");
- var tabstopOptions = {
- tabstop: function(data) {
- var group = parseInt(data.group, 10);
- var isZero = group === 0;
- if (isZero)
- group = ++zeroBase;
- else
- group += base;
- var placeholder = data.placeholder;
- if (placeholder) {
- // recursively update nested tabstops
- placeholder = ts.processText(placeholder, tabstopOptions);
- }
- var result = '${' + group + (placeholder ? ':' + placeholder : '') + '}';
- if (isZero) {
- lastZero = range.create(data.start, result);
- }
- return result;
- },
- escape: function(ch) {
- if (ch == '$') return '\\$';
- if (ch == '\\') return '\\\\';
- return ch;
- }
- };
- value = ts.processText(value, tabstopOptions);
- if (settings.variables['insert_final_tabstop'] && !/\$\{0\}$/.test(value)) {
- value += '${0}';
- } else if (lastZero) {
- value = emmet.require('utils').replaceSubstring(value, '${0}', lastZero);
- }
-
- return value;
- }
- };
- var keymap = {
- expand_abbreviation: {"mac": "ctrl+alt+e", "win": "alt+e"},
- match_pair_outward: {"mac": "ctrl+d", "win": "ctrl+,"},
- match_pair_inward: {"mac": "ctrl+j", "win": "ctrl+shift+0"},
- matching_pair: {"mac": "ctrl+alt+j", "win": "alt+j"},
- next_edit_point: "alt+right",
- prev_edit_point: "alt+left",
- toggle_comment: {"mac": "command+/", "win": "ctrl+/"},
- split_join_tag: {"mac": "shift+command+'", "win": "shift+ctrl+`"},
- remove_tag: {"mac": "command+'", "win": "shift+ctrl+;"},
- evaluate_math_expression: {"mac": "shift+command+y", "win": "shift+ctrl+y"},
- increment_number_by_1: "ctrl+up",
- decrement_number_by_1: "ctrl+down",
- increment_number_by_01: "alt+up",
- decrement_number_by_01: "alt+down",
- increment_number_by_10: {"mac": "alt+command+up", "win": "shift+alt+up"},
- decrement_number_by_10: {"mac": "alt+command+down", "win": "shift+alt+down"},
- select_next_item: {"mac": "shift+command+.", "win": "shift+ctrl+."},
- select_previous_item: {"mac": "shift+command+,", "win": "shift+ctrl+,"},
- reflect_css_value: {"mac": "shift+command+r", "win": "shift+ctrl+r"},
- encode_decode_data_url: {"mac": "shift+ctrl+d", "win": "ctrl+'"},
- // update_image_size: {"mac": "shift+ctrl+i", "win": "ctrl+u"},
- // expand_as_you_type: "ctrl+alt+enter",
- // wrap_as_you_type: {"mac": "shift+ctrl+g", "win": "shift+ctrl+g"},
- expand_abbreviation_with_tab: "Tab",
- wrap_with_abbreviation: {"mac": "shift+ctrl+a", "win": "shift+ctrl+a"}
- };
- var editorProxy = new AceEmmetEditor();
- exports.commands = new HashHandler();
- exports.runEmmetCommand = function runEmmetCommand(editor) {
- try {
- editorProxy.setupContext(editor);
- var actions = emmet.require("actions");
-
- if (this.action == "expand_abbreviation_with_tab") {
- if (!editor.selection.isEmpty())
- return false;
- }
-
- if (this.action == "wrap_with_abbreviation") {
- // without setTimeout prompt doesn't work on firefox
- return setTimeout(function() {
- actions.run("wrap_with_abbreviation", editorProxy);
- }, 0);
- }
-
- var pos = editor.selection.lead;
- var token = editor.session.getTokenAt(pos.row, pos.column);
- if (token && /\btag\b/.test(token.type))
- return false;
-
- var result = actions.run(this.action, editorProxy);
- } catch(e) {
- if (!emmet) {
- load(runEmmetCommand.bind(this, editor));
- return true;
- }
- editor._signal("changeStatus", typeof e == "string" ? e : e.message);
- console.log(e);
- result = false;
- }
- return result;
- };
- for (var command in keymap) {
- exports.commands.addCommand({
- name: "emmet:" + command,
- action: command,
- bindKey: keymap[command],
- exec: exports.runEmmetCommand,
- multiSelectAction: "forEach"
- });
- }
- exports.updateCommands = function(editor, enabled) {
- if (enabled) {
- editor.keyBinding.addKeyboardHandler(exports.commands);
- } else {
- editor.keyBinding.removeKeyboardHandler(exports.commands);
- }
- };
- exports.isSupportedMode = function(mode) {
- if (!mode) return false;
- if (mode.emmetConfig) return true;
- var id = mode.$id || mode;
- return /css|less|scss|sass|stylus|html|php|twig|ejs|handlebars/.test(id);
- };
- exports.isAvailable = function(editor, command) {
- if (/(evaluate_math_expression|expand_abbreviation)$/.test(command))
- return true;
- var mode = editor.session.$mode;
- var isSupported = exports.isSupportedMode(mode);
- if (isSupported && mode.$modes) {
- // TODO refactor mode delegates to make this simpler
- try {
- editorProxy.setupContext(editor);
- if (/js|php/.test(editorProxy.getSyntax()))
- isSupported = false;
- } catch(e) {}
- }
- return isSupported;
- }
- var onChangeMode = function(e, target) {
- var editor = target;
- if (!editor)
- return;
- var enabled = exports.isSupportedMode(editor.session.$mode);
- if (e.enableEmmet === false)
- enabled = false;
- if (enabled)
- load();
- exports.updateCommands(editor, enabled);
- };
- var load = function(cb) {
- if (typeof emmetPath == "string") {
- require("ace/config").loadModule(emmetPath, function() {
- emmetPath = null;
- cb && cb();
- });
- }
- };
- exports.AceEmmetEditor = AceEmmetEditor;
- require("ace/config").defineOptions(Editor.prototype, "editor", {
- enableEmmet: {
- set: function(val) {
- this[val ? "on" : "removeListener"]("changeMode", onChangeMode);
- onChangeMode({enableEmmet: !!val}, this);
- },
- value: true
- }
- });
- exports.setCore = function(e) {
- if (typeof e == "string")
- emmetPath = e;
- else
- emmet = e;
- };
- });
|