464 lines
16 KiB
JavaScript
Executable File
464 lines
16 KiB
JavaScript
Executable File
/* ***** 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;
|
|
var resources = emmet.resources || emmet.require("resources");
|
|
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() {
|
|
var resources = emmet.resources || emmet.require("resources");
|
|
switch (this.getSyntax()) {
|
|
case "css": return "css";
|
|
case "xml":
|
|
case "xsl":
|
|
return "xml";
|
|
case "html":
|
|
var profile = 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 ts = emmet.tabStops || emmet.require('tabStops');
|
|
var resources = emmet.resources || emmet.require("resources");
|
|
var settings = 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 = [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) {
|
|
var common = emmet.utils ? emmet.utils.common : emmet.require('utils');
|
|
value = common.replaceSubstring(value, '${0}', lastZero[0], lastZero[1]);
|
|
}
|
|
|
|
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.actions || emmet.require("actions");
|
|
|
|
if (this.action == "expand_abbreviation_with_tab") {
|
|
if (!editor.selection.isEmpty())
|
|
return false;
|
|
var pos = editor.selection.lead;
|
|
var token = editor.session.getTokenAt(pos.row, pos.column);
|
|
if (token && /\btag\b/.test(token.type))
|
|
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 result = actions.run(this.action, editorProxy);
|
|
} catch(e) {
|
|
if (!emmet) {
|
|
exports.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)
|
|
exports.load();
|
|
exports.updateCommands(editor, enabled);
|
|
};
|
|
|
|
exports.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;
|
|
};
|
|
});
|
|
|