/* ***** 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; Editor.prototype.indexToPosition = function(index) { return this.session.doc.indexToPosition(index); }; Editor.prototype.positionToIndex = function(pos) { return this.session.doc.positionToIndex(pos); }; /** * 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 start * and end properties. If there's no selection, should return * object with start and end 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(); return { start: this.ace.positionToIndex(range.start), end: this.ace.positionToIndex(range.end) }; }, /** * Creates selection from start to end character * indexes. If end is ommited, this method should place caret * and start index * @param {Number} start * @param {Number} [end] * @example * editor.createSelection(10, 40); * * //move caret to 15th character * editor.createSelection(15); */ createSelection: function(start, end) { this.ace.selection.setRange({ start: this.ace.indexToPosition(start), end: this.ace.indexToPosition(end) }); }, /** * Returns current line's start and end indexes as object with start * and end properties * @return {Object} * @example * var range = editor.getCurrentLineRange(); * alert(range.start + ', ' + range.end); */ getCurrentLineRange: function() { var row = this.ace.getCursorPosition().row; var lineLength = this.ace.session.getLine(row).length; var index = this.ace.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.positionToIndex(pos); }, /** * Set new caret position * @param {Number} index Caret position */ setCaretPos: function(index){ var pos = this.ace.indexToPosition(index); this.ace.clearSelection(); this.ace.selection.moveCursorToPosition(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 start to * end index). If value contains * caret_placeholder, the editor will put caret into * this position. If you skip start and end * arguments, the whole target's content will be replaced with * value. * * If you pass start argument only, * the value will be placed at start string * index of current content. * * If you pass start and end arguments, * the corresponding substring of current target's content will be * replaced with value. * @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 value */ 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 range = Range.fromPoints(editor.indexToPosition(start), editor.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(/]+XHTML/i) != -1 ? "xhtml": "html"; return profile; } return "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(editor) { editorProxy.setupContext(editor); if (editorProxy.getSyntax() == "php") return false; 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); } try { var result = actions.run(this.action, editorProxy); } catch(e) { editor._signal("changeStatus", typeof e == "string" ? e : e.message); console.log(e); } return result; }; for (var command in keymap) { exports.commands.addCommand({ name: "emmet:" + command, action: command, bindKey: keymap[command], exec: exports.runEmmetCommand, multiSelectAction: "forEach" }); } var onChangeMode = function(e, target) { var editor = target; if (!editor) return; var modeId = editor.session.$modeId; var enabled = modeId && /css|less|scss|sass|stylus|html|php/.test(modeId); if (e.enableEmmet === false) enabled = false; if (enabled) editor.keyBinding.addKeyboardHandler(exports.commands); else editor.keyBinding.removeKeyboardHandler(exports.commands); }; 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) {emmet = e;}; });