diff --git a/.jshintrc b/.jshintrc index 3dd023a..5adcad8 100644 --- a/.jshintrc +++ b/.jshintrc @@ -9,7 +9,7 @@ "noarg": true, "noempty": true, "nonew": true, - "quotmark": false, + "quotmark": "double", "undef": true, "unused": true, "strict": false, @@ -31,6 +31,8 @@ "white": true, "globals": { - + "define": true, + "brackets": true, + "Mustache": true } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 952da65..3c814aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## 1.2.0 +* Added support for snippet keyboard shortcuts + ## 1.1.0 * Added new settings panel - [#34](https://github.com/jrowny/brackets-snippets/pull/34) * Added support for inline snippets - [#15](https://github.com/jrowny/brackets-snippets/issues/15) diff --git a/LICENSE b/LICENSE index f8d2466..63338f5 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,8 @@ The MIT License (MIT) Copyright (c) 2014 Jonathan Rowny and other contributors -https://github.com/jrowny/brackets-snippets/graphs/contributors +http://www.jonathanrowny.com +http://github.com/jrowny/brackets-snippets/graphs/contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/README.md b/README.md index 9b77fc0..9c66b30 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,40 @@ for (x = 0; x < myArray.length; x++) { If you omit parameters, an inline form will appear. Use `ESC` to close the inline form or `ENTER` to complete the insertion. -Adding Snippets -=============== -You can create new JSON files in the ```data``` directory or you can edit the existing ```javascript.json``` file. Your JSON files can reference template files if they have a `.snippet` extension and are in the `data\snippets` directory. See html5.snippet for an example. +![Example animation](https://raw.github.com/jrowny/brackets-snippets/master/docs/angularExample.gif) + +Own snippets +============ + +It's recommended to copy data directory from extension to your own place and link to it through settings dialog. +You can edit the data directory inside the extension __but this will be overridden on every extension update__. + +You can create new JSON files in the ```data``` directory or you can edit the existing ```javascript.json``` file. +Your JSON files can reference template files if they have a `.snippet` extension and are in the `data\snippets` directory. See html5.snippet for an example. + +Snippets with keyboard shortcuts +================================ + +You can defined own shortcuts for immediate snippet execution on current cursor position like this: + +``` +{ + "name": "Sample inline script for console logging", + "trigger": "log", + "usage": "log x", + "description": "Log a message into console", + "template": "console.log($${message});!!{cursor}", + "inline": true, + "shortcut": "Alt-L" +} +``` + +And then simply use it while working on your code: + +- using arguments on the line: + +![Shortcut sample animation](https://raw.github.com/jrowny/brackets-snippets/master/docs/snippetShortcutArgs.gif) + +- or using snippet widget: + +![Shortcut sample animation 2](https://raw.github.com/jrowny/brackets-snippets/master/docs/snippetShortcutWidget.gif) diff --git a/data/javascript.json b/data/javascript.json index 432ca97..72e258e 100644 --- a/data/javascript.json +++ b/data/javascript.json @@ -40,6 +40,7 @@ "usage": "log x", "description": "Log a message into console", "template": "console.log($${message});!!{cursor}", - "inline": true + "inline": true, + "shortcut": "Alt-L" } ] diff --git a/docs/angularExample.gif b/docs/angularExample.gif new file mode 100644 index 0000000..a0a82b7 Binary files /dev/null and b/docs/angularExample.gif differ diff --git a/docs/snippetShortcutArgs.gif b/docs/snippetShortcutArgs.gif new file mode 100644 index 0000000..dffcccc Binary files /dev/null and b/docs/snippetShortcutArgs.gif differ diff --git a/docs/snippetShortcutWidget.gif b/docs/snippetShortcutWidget.gif new file mode 100644 index 0000000..31af73d Binary files /dev/null and b/docs/snippetShortcutWidget.gif differ diff --git a/main.js b/main.js index fe65b43..96b1751 100644 --- a/main.js +++ b/main.js @@ -1,377 +1,88 @@ -/* - * Copyright (c) 2013 Jonathan Rowny. All rights reserved. - * http://www.jonathanrowny.com - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - * DEALINGS IN THE SOFTWARE. - */ - -/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ -/*global define, brackets, $, Mustache */ - define(function (require, exports, module) { - var _ = brackets.getModule("thirdparty/lodash"), - AppInit = brackets.getModule("utils/AppInit"), - Commands = brackets.getModule("command/Commands"), - CommandManager = brackets.getModule("command/CommandManager"), - EditorManager = brackets.getModule("editor/EditorManager"), - DocumentManager = brackets.getModule("document/DocumentManager"), - ExtensionUtils = brackets.getModule("utils/ExtensionUtils"), - FileSystem = brackets.getModule("filesystem/FileSystem"), - KeyBindingManager = brackets.getModule("command/KeyBindingManager"), - FileUtils = brackets.getModule("file/FileUtils"), - Menus = brackets.getModule("command/Menus"), - PanelManager = brackets.getModule("view/PanelManager"); - - // Local modules - var InlineSnippetForm = require("src/InlineSnippetForm"), - Preferences = require("src/Preferences"), - SettingsDialog = require("src/SettingsDialog"), - SnippetPresets = require("src/SnippetPresets"), - panelHtml = require("text!templates/bottom-panel.html"), - snippetsHTML = require("text!templates/snippets-table.html"); - - //Snippets array - var snippets = [], - //directory where snippets are - snippetsDirectory = Preferences.get("snippetsDirectory").replace(/\\/g, "/"), - $snippetsPanel, - $snippetsContent, - panel; - - //commands - var SNIPPET_EXECUTE = "snippets.execute", - VIEW_HIDE_SNIPPETS = "snippets.hideSnippets"; - - function _handleHideSnippets() { - if (panel.isVisible()) { - panel.hide(); - CommandManager.get(VIEW_HIDE_SNIPPETS).setChecked(false); - } else { - panel.show(); - CommandManager.get(VIEW_HIDE_SNIPPETS).setChecked(true); - } - EditorManager.resizeEditor(); - } - - function inlineSnippetFormProvider(hostEditor, props, snippet) { - var result = new $.Deferred(); - - var snippetForm = new InlineSnippetForm(props, snippet); - snippetForm.load(hostEditor); - result.resolve(snippetForm); - - return result.promise(); - } - - function _parseArgs(str) { - str = str.trim(); - - var result = [], - current = "", - inQuotes = false, - quotes = ['"', "'"]; - - for (var i = 0, l = str.length; i < l; i++) { - if (str[i] === " " && inQuotes === false) { - if (current.length > 0) { - result.push(current); - current = ""; - } - continue; - } - if (inQuotes && str[i] === inQuotes) { - inQuotes = false; - } - if (current.length === 0 && quotes.indexOf(str[i]) !== -1) { - inQuotes = str[i]; - } - current += str[i]; - } - result.push(current); - - return result; - } - - function _handleSnippet(props) { - var editor = EditorManager.getCurrentFullEditor(), - document = DocumentManager.getCurrentDocument(), - pos = editor.getCursorPos(), - line = document.getLine(pos.line), - preInline = null, - postInline = null; - - if (!props) { - props = _parseArgs(line); - } - - function completeInsert(editor, pos, output) { - var s, - x, - cursorPos, - cursorOffset; - - // add back text that was found before inline snippet - if (preInline) { - output = preInline.join(" ") + " " + output; - } - if (postInline) { - output = output + " " + postInline.join(" "); - } - - var lines = output.split("\n"); - - //figure out cursor pos, remove cursor marker - for (s = 0; s < lines.length; s++) { - cursorOffset = lines[s].indexOf('!!{cursor}'); - if (cursorOffset >= 0) { - cursorPos = s; - output = output.replace('!!{cursor}', ''); - break; - } - } - - //do insertion - document.replaceRange(output + "\n", {line: pos.line, ch: 0}, {line: pos.line, ch: 0}); - - //set cursor - editor._codeMirror.setCursor(pos.line + cursorPos, cursorOffset); - - //indent lines - for (x = 0; x < lines.length; x++) { - editor._codeMirror.indentLine(pos.line + x); - } - - //give focus back - EditorManager.focusEditor(); - } - function startInsert(output) { - //we don't need to see the trigger text - CommandManager.execute(Commands.EDIT_DELETE_LINES); - - //find variables - var tmp = output.match(/\$\$\{[0-9A-Z_a-z]{1,32}\}/g); - //remove duplicate variables - var snippetVariables = [], - j; - - if (tmp && tmp.length > 0) { - for (j = 0; j < tmp.length; j++) { - if ($.inArray(tmp[j], snippetVariables) === -1) { - snippetVariables.push(tmp[j]); - } - } - } - - var variablesDifference = props.length - 1 - snippetVariables.length; - - if (variablesDifference > 0) { - // we have more variables than we require - var mid = snippetVariables.length + 1; - postInline = props.slice(mid); - props = props.slice(0, mid); - variablesDifference = 0; - } - - if (variablesDifference === 0) { - // we have exactly as many variables as we need - for (var x = 0; x < snippetVariables.length; x++) { - //even my escapes have escapes - var re = new RegExp(snippetVariables[x].replace('$${', '\\$\\$\\{').replace('}', '\\}'), 'g'); - output = output.replace(re, props[x + 1]); - } - completeInsert(editor, pos, output); - } else { - // we have less variables than we need - var snippetPromise, - result = new $.Deferred(); - snippetPromise = inlineSnippetFormProvider(editor, snippetVariables, output); - - snippetPromise.done(function (inlineWidget) { - var newPos = {line: pos.line - 1, ch: pos.ch}; - editor.addInlineWidget(newPos, inlineWidget); - var inlineComplete = function () { - var z; - for (z = 0; z < snippetVariables.length; z++) { - //even my escapes have escapes - var re = new RegExp(snippetVariables[z].replace('$${', '\\$\\$\\{').replace('}', '\\}'), 'g'); - output = output.replace(re, inlineWidget.$form.find('.snipvar-' + snippetVariables[z].replace('$${', '').replace('}', '')).val()); - } - - completeInsert(editor, pos, output); - }; - inlineWidget.$form.on('complete', function () { - inlineComplete(); - inlineWidget.close(); - }); - }).fail(function () { - result.reject(); - console.log("Can't create inline snippet form"); - }); - } - } - - function readSnippetFromFile(fileName) { - var snippetFilePath = snippetsDirectory + '/snippets/' + fileName; - FileSystem.resolve(snippetFilePath, function (err, snippetFile) { - if (err) { - FileUtils.showFileOpenError(err, snippetFilePath); - return; - } - - snippetFile.read(function (err, text) { - if (err) { - FileUtils.showFileOpenError(err, snippetFile.fullPath); - return; - } - - startInsert(SnippetPresets.execute(text)); - }); + // Brackets modules + var _ = brackets.getModule("thirdparty/lodash"), + AppInit = brackets.getModule("utils/AppInit"), + CommandManager = brackets.getModule("command/CommandManager"), + ExtensionUtils = brackets.getModule("utils/ExtensionUtils"), + FileSystem = brackets.getModule("filesystem/FileSystem"), + FileUtils = brackets.getModule("file/FileUtils"), + KeyBindingManager = brackets.getModule("command/KeyBindingManager"); + + // Dependencies + var Preferences = require("src/Preferences"), + SnippetPanel = require("src/SnippetPanel"), + SnippetInsertion = require("src/SnippetInsertion"); + + function finalizeSnippetsLoading(arrays) { + // merge all results into snippets array + var snippets = arrays.reduce(function (snippets, arr) { + return snippets.concat(arr); + }, []); + + // register keyboard shortcuts for snippets + _.filter(snippets, function (snippet) { + return typeof snippet.shortcut === "string"; + }).forEach(function (snippet) { + var commandName = "Run snippet " + snippet.trigger, + commandString = "snippets.execute." + snippet.trigger; + CommandManager.register(commandName, commandString, function () { + SnippetInsertion.triggerSnippet(snippet); }); - } - - if (props.length) { - //try to find the snippet, given the trigger text - var i, - triggers = _.pluck(snippets, "trigger"); - //go in backwards order for a case there is an inline snippet along the way - for (i = props.length - 1; i > 0; i--) { - - var io, - requireInline = true; - - // launch non-inline in inline mode snippets when %trigger is found - if (props[i][0] === "%") { - requireInline = false; - io = triggers.indexOf(props[i].substring(1)); - } else { - io = triggers.indexOf(props[i]); - } - - if (io !== -1 && (snippets[io].inline || !requireInline)) { - // found inline snippet - preInline = props.slice(0, i); - props = props.slice(i); - startInsert(SnippetPresets.execute(snippets[io].template)); - return; - } - } - //no inline snippet found so look for any snippet that matches props[0] - //it can also have % as a prefix so remove - if (props[0][0] === "%") { - props[0] = props[0].substring(1); - } - var snippet = _.find(snippets, function (s) { - return s.trigger === props[0]; - }); - if (snippet) { - var output = snippet.template; - if (output.indexOf('.snippet') === output.length - 8) { - readSnippetFromFile(output); - } else { - startInsert(SnippetPresets.execute(output)); - } - } - } - } - - //builds the snippets table - function setupSnippets() { - var s = Mustache.render(panelHtml); - panel = PanelManager.createBottomPanel(VIEW_HIDE_SNIPPETS, $(s), 100); - panel.hide(); - - $snippetsPanel = $('#snippets'); - $snippetsContent = $snippetsPanel.find(".resizable-content"); - $snippetsPanel - .on("click", ".snippets-settings", function () { - SettingsDialog.show(); - }) - .on("click", ".snippets-close", function () { - CommandManager.execute(VIEW_HIDE_SNIPPETS); - }); - } - - function finalizeSnippetsTable() { - var i, - len, - $snippetsTable; - for (i = 0, len = arguments.length; i < len; i++) { - if (arguments[i] && arguments[i].length) { - snippets = snippets.concat(arguments[i]); - } - } - $snippetsTable = Mustache.render(snippetsHTML, {"snippets" : snippets}); - $snippetsPanel.find('.resizable-content').append($snippetsTable); - $snippetsPanel.find('.snippets-trigger').on('click', function () { - CommandManager.execute(SNIPPET_EXECUTE, [$(this).attr('data-trigger')]); - }); - $snippetsPanel.find('.snippets-source').on('click', function () { - CommandManager.execute(Commands.FILE_OPEN, { fullPath: snippetsDirectory + "/" + $(this).attr('data-source') }); + KeyBindingManager.addBinding(commandString, snippet.shortcut); }); + + return snippets; } - - //parse a JSON file with a snippet in it - function loadSnippet(fileEntry) { - var result = new $.Deferred(); + + function loadSnippetsFromFile(fileEntry) { + var deferred = new $.Deferred(); fileEntry.read(function (err, text) { if (err) { FileUtils.showFileOpenError(err, fileEntry.fullPath); - result.reject(err); - return; + return deferred.reject(err); } + var newSnippets; + //TODO: a better check for valid snippets try { - //TODO: a better check for valid snippets - var newSnippets = JSON.parse(text); - newSnippets.forEach(function (item) { - item.source = fileEntry.name; - }); - result.resolve(newSnippets); + newSnippets = JSON.parse(text); } catch (err) { console.error("Can't parse snippets from " + fileEntry.fullPath + " - " + err); - result.reject(err); + return deferred.reject(err); } + + newSnippets.forEach(function (item) { + item.source = fileEntry.name; + }); + deferred.resolve(newSnippets); }); - return result; + return deferred.promise(); } - CommandManager.register("Run Snippet", SNIPPET_EXECUTE, _handleSnippet); - CommandManager.register("Show Snippets", VIEW_HIDE_SNIPPETS, _handleHideSnippets); - - function loadSnippets() { + function getSnippetsDirectory() { + var snippetsDirectory = Preferences.get("snippetsDirectory").replace(/\\/g, "/"); if (!FileSystem.isAbsolutePath(snippetsDirectory)) { snippetsDirectory = FileUtils.getNativeModuleDirectoryPath(module) + "/" + snippetsDirectory; } + return snippetsDirectory; + } + + function loadAllSnippetsFromDataDirectory() { + var deferred = new $.Deferred(), + snippetsDirectory = getSnippetsDirectory(); + //loop through the directory to load snippets FileSystem.resolve(snippetsDirectory, function (err, rootEntry) { if (err) { console.error("[Snippets] Error -- could not open snippets directory: " + snippetsDirectory); - console.error(err); - return; + return deferred.reject(err); } rootEntry.getContents(function (err, entries) { if (err) { console.error("[Snippets] Error -- could not read snippets directory: " + snippetsDirectory); - console.error(err); - return; + return deferred.reject(err); } var loading = _.compact(entries.map(function (entry) { @@ -383,39 +94,28 @@ define(function (require, exports, module) { //ignore directories return; } - return loadSnippet(entry); + return loadSnippetsFromFile(entry); })); - $.when.apply(module, loading).done(finalizeSnippetsTable); + $.when.apply(module, loading).done(function () { + deferred.resolve(finalizeSnippetsLoading(_.toArray(arguments))); + }); }); }); + return deferred.promise(); } AppInit.appReady(function () { - //add the HTML UI - setupSnippets(); - - //load css ExtensionUtils.loadStyleSheet(module, "styles/snippets.css"); - - //add the menu and keybinding for view/hide - var menu = Menus.getMenu(Menus.AppMenuBar.VIEW_MENU); - menu.addMenuItem(VIEW_HIDE_SNIPPETS, Preferences.get("showSnippetsPanelShortcut"), Menus.AFTER, Commands.VIEW_HIDE_SIDEBAR); - - // Add toolbar icon - $("") - .attr({ - id: "snippets-enable-icon", - href: "#" - }) - .click(_handleHideSnippets) - .appendTo($("#main-toolbar .buttons")); - - //add the keybinding - KeyBindingManager.addBinding(SNIPPET_EXECUTE, Preferences.get("triggerSnippetShortcut")); - - //load snippets - loadSnippets(); + SnippetPanel.init(); + loadAllSnippetsFromDataDirectory().done(function (snippets) { + SnippetPanel.renderTable(snippets); + SnippetInsertion.init(snippets); + }).fail(function (err) { + console.error(err); + }); }); + // Public API + exports.getSnippetsDirectory = getSnippetsDirectory; }); diff --git a/package.json b/package.json index bb527bd..6fd3d77 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "license": "MIT", "author": "Jonathan Rowny", - "version": "1.1.0-dev", + "version": "1.2.0", "engines": { "brackets": ">=0.36" }, "devDependencies": { diff --git a/src/InlineSnippetForm.js b/src/InlineSnippetForm.js index b5c4c35..01e3024 100644 --- a/src/InlineSnippetForm.js +++ b/src/InlineSnippetForm.js @@ -1,34 +1,9 @@ -/* - * Copyright (c) 2013 Jonathan Rowny. All rights reserved. - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - * DEALINGS IN THE SOFTWARE. - * - */ - - -/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ -/*global define, brackets, $, window */ - define(function (require, exports, module) { - 'use strict'; - // Load Brackets modules + "use strict"; + + // Brackets modules var InlineWidget = brackets.getModule("editor/InlineWidget").InlineWidget; + function InlineSnippetForm(props, snippet) { this.props = props; this.snippet = snippet; @@ -37,8 +12,8 @@ define(function (require, exports, module) { } function textWidth(text) { - var html = $('' + text + ''); - $('body').append(html); + var html = $("" + text + ""); + $("body").append(html); var width = html.width(); html.remove(); return width; @@ -59,20 +34,20 @@ define(function (require, exports, module) { x; this.parentClass.load.call(this, hostEditor); - this.$form = $('
'); + this.$form = $("
"); function formElement(property) { - property = property.replace('$${', '').replace('}', ''); - return ''; + property = property.replace("$${", "").replace("}", ""); + return ""; } //make our snippet look nice in the editor - htmlOutput = htmlOutput.replace(//g, '>') - .replace(/(\r\n|\n|\r)/gm, '
') - .replace(/\t/g, ' ') - .replace('!!{cursor}', '') - .replace(/ /g, ' '); + htmlOutput = htmlOutput.replace(//g, ">") + .replace(/(\r\n|\n|\r)/gm, "
") + .replace(/\t/g, " ") + .replace("!!{cursor}", "") + .replace(/ /g, " "); //turn the snippets into form fields for (x = 0; x < this.props.length; x++) { @@ -82,20 +57,20 @@ define(function (require, exports, module) { this.$form.append(htmlOutput); //size the inputs to the placeholders and changing text - this.$form.find('input').each(function () { + this.$form.find("input").each(function () { var $input = $(this); var newWidth = 0; - $input.width(textWidth($(this).attr('placeholder'))); + $input.width(textWidth($(this).attr("placeholder"))); $input.keyup(function () { - if ($input.is(':focus')) { + if ($input.is(":focus")) { if ($(this).val() === "") { - newWidth = textWidth($(this).attr('placeholder')); + newWidth = textWidth($(this).attr("placeholder")); } else { //dash gives it some extra space while typing newWidth = textWidth($(this).val()); } - $input.parent().find('.snipvar-' + $(this).attr('data-snippet')).width(newWidth); - $input.parent().find('.snipvar-' + $(this).attr('data-snippet')).not(this).val($(this).val()); + $input.parent().find(".snipvar-" + $(this).attr("data-snippet")).width(newWidth); + $input.parent().find(".snipvar-" + $(this).attr("data-snippet")).not(this).val($(this).val()); } }); }); @@ -106,24 +81,26 @@ define(function (require, exports, module) { if (e.which === 13) { e.stopImmediatePropagation(); e.preventDefault(); - $(this).trigger('complete'); + $(this).trigger("complete"); } else if (e.which === 9) { //we will control the tabing e.stopImmediatePropagation(); e.preventDefault(); //select the next empty element unless there is no next element... or the next element is the current element - var next = $(this).find('input').filter(function () { return $(this).val() === ""; }); - if (next.length && !$(next[0]).is(':focus')) { + var next = $(this).find("input").filter(function () { return $(this).val() === ""; }); + if (next.length && !$(next[0]).is(":focus")) { $(next[0]).focus(); } else { - $(this).trigger('complete'); + $(this).trigger("complete"); } } }); - this.$htmlContent.addClass('snippet-form-widget'); + this.$htmlContent.addClass("snippet-form-widget"); this.$htmlContent.append(this.$form); + + return this; }; InlineSnippetForm.prototype.close = function () { @@ -132,7 +109,7 @@ define(function (require, exports, module) { InlineSnippetForm.prototype.onAdded = function () { InlineSnippetForm.prototype.parentClass.onAdded.apply(this, arguments); window.setTimeout(this._sizeEditorToContent.bind(this)); - this.$form.find('input').first().focus(); + this.$form.find("input").first().focus(); }; InlineSnippetForm.prototype._sizeEditorToContent = function () { diff --git a/src/Preferences.js b/src/Preferences.js index 646bf78..158131b 100644 --- a/src/Preferences.js +++ b/src/Preferences.js @@ -1,13 +1,15 @@ -/*global brackets, define */ - define(function (require, exports, module) { "use strict"; - var PREFERENCES_KEY = "brackets-snippets", - _ = brackets.getModule("thirdparty/lodash"), - PreferencesManager = brackets.getModule("preferences/PreferencesManager"), - prefs = PreferencesManager.getExtensionPrefs(PREFERENCES_KEY); + // Brackets modules + var _ = brackets.getModule("thirdparty/lodash"), + PreferencesManager = brackets.getModule("preferences/PreferencesManager"); + + // Module constants + var PREFERENCES_KEY = "brackets-snippets"; + // Module variables + var prefs = PreferencesManager.getExtensionPrefs(PREFERENCES_KEY); var defaultPreferences = { "snippetsDirectory": { "type": "string", "value": "data" }, "triggerSnippetShortcut": { "type": "string", "value": "Ctrl-Alt-Space" }, diff --git a/src/SettingsDialog.js b/src/SettingsDialog.js index 5249b5a..62c239d 100644 --- a/src/SettingsDialog.js +++ b/src/SettingsDialog.js @@ -1,15 +1,17 @@ -/*global $, brackets, define, Mustache */ - define(function (require, exports) { "use strict"; - var CommandManager = brackets.getModule("command/CommandManager"), - Dialogs = brackets.getModule("widgets/Dialogs"), - Preferences = require("./Preferences"), - Strings = require("../strings"), - settingsDialogTemplate = require("text!templates/settings-dialog.html"), - questionDialogTemplate = require("text!templates/question-dialog.html"); + // Brackets modules + var CommandManager = brackets.getModule("command/CommandManager"), + Dialogs = brackets.getModule("widgets/Dialogs"); + + // Dependencies + var Preferences = require("./Preferences"), + Strings = require("../strings"), + settingsDialogTemplate = require("text!templates/settings-dialog.html"), + questionDialogTemplate = require("text!templates/question-dialog.html"); + // Module variables var dialog, $dialog; diff --git a/src/SnippetInsertion.js b/src/SnippetInsertion.js new file mode 100644 index 0000000..4e4eff7 --- /dev/null +++ b/src/SnippetInsertion.js @@ -0,0 +1,319 @@ +define(function (require, exports) { + "use strict"; + + // Brackets modules + var _ = brackets.getModule("thirdparty/lodash"), + CommandManager = brackets.getModule("command/CommandManager"), + DocumentManager = brackets.getModule("document/DocumentManager"), + EditorManager = brackets.getModule("editor/EditorManager"), + FileSystem = brackets.getModule("filesystem/FileSystem"), + FileUtils = brackets.getModule("file/FileUtils"), + KeyBindingManager = brackets.getModule("command/KeyBindingManager"); + + // Dependencies + var Main = require("main"), + InlineSnippetForm = require("src/InlineSnippetForm"), + Preferences = require("src/Preferences"), + SnippetPresets = require("src/SnippetPresets"); + + // Module variables + var snippets; + + // create context for inserting snippets that is passed between the functions + function _createContext() { + var editor = EditorManager.getCurrentFullEditor(), + pos = editor.getCursorPos(), + document = DocumentManager.getCurrentDocument(), + line = document.getLine(pos.line); + return { + editor: editor, + pos: pos, + document: document, + line: line + }; + } + + // parse parts of the line as possible arguments or snippet triggers + function _parseArgs(str) { + var result = [], + current = "", + inQuotes = false, + quotes = ["\"", "'"]; + for (var i = 0, l = str.length; i < l; i++) { + if (str[i] === " " && inQuotes === false) { + if (current.length > 0) { + result.push({ + str: current, + ch: i - current.length + }); + current = ""; + } + continue; + } + if (inQuotes && str[i] === inQuotes) { + inQuotes = false; + } + if (current.length === 0 && quotes.indexOf(str[i]) !== -1) { + inQuotes = str[i]; + } + current += str[i]; + } + if (current) { + result.push({ + str: current, + ch: i - current.length + }); + } + return result; + } + + // loads snippets by filename from snippets directory, currently hard-coded + function _readSnippetFromFile(fileName) { + var deferred = new $.Deferred(), + snippetFilePath = Main.getSnippetsDirectory() + "/snippets/" + fileName; + FileSystem.resolve(snippetFilePath, function (err, snippetFile) { + if (err) { + FileUtils.showFileOpenError(err, snippetFilePath); + return deferred.reject(err); + } + + snippetFile.read(function (err, text) { + if (err) { + FileUtils.showFileOpenError(err, snippetFile.fullPath); + return deferred.reject(err); + } + + deferred.resolve(text); + }); + }); + return deferred.promise(); + } + + // loads snippet from file if required, or resolves immediately + function _loadSnippetTemplate(template) { + if (template.indexOf(".snippet") === template.length - 8) { + return _readSnippetFromFile(template); + } else { + return $.Deferred().resolve(template).promise(); + } + } + + function _getVariablesFromTemplate(template) { + var snippetVariables = []; + + // find variables + var tmp = template.match(/\$\$\{[0-9A-Z_a-z]{1,32}\}/g); + // remove duplicate variables + if (tmp && tmp.length > 0) { + for (var j = 0; j < tmp.length; j++) { + if ($.inArray(tmp[j], snippetVariables) === -1) { + snippetVariables.push(tmp[j]); + } + } + } + + return snippetVariables; + } + + function _getInlineSnippetForm(context) { + var hostEditor = context.editor; + var props = context.templateVariables; + var snippet = context.template; + return $.when(new InlineSnippetForm(props, snippet).load(hostEditor)); + } + + // finishes snippet insertion + function _insertSnippet(context) { + var cursorOffsetLine, + cursorOffsetChar, + template = context.template, + snippetLines = template.split("\n"); + + // figure out cursor pos, remove cursor marker + for (var s = 0; s < snippetLines.length; s++) { + cursorOffsetChar = snippetLines[s].indexOf("!!{cursor}"); + if (cursorOffsetChar >= 0) { + cursorOffsetLine = s; + template = template.replace("!!{cursor}", ""); + break; + } + } + + var line = context.pos.line, + chFrom = context.snippetTrigger.ch, + chTo = context.snippetTrigger.ch + context.snippetTrigger.str.length; + + // include used arguments in replace range + if (context.snippetArgs.length > 0) { + var firstStart = _.first(context.snippetArgs).ch; + if (firstStart < chFrom) { + chFrom = firstStart; + } + var last = _.last(context.snippetArgs), + lastEnd = last.ch + last.str.length; + if (lastEnd > chTo) { + chTo = lastEnd; + } + } + + // do insertion + context.document.replaceRange(template, {line: line, ch: chFrom}, {line: line, ch: chTo}); + + // set cursor + context.editor._codeMirror.setCursor(line + cursorOffsetLine, chFrom + cursorOffsetChar); + + // indent lines + for (var x = 0; x < snippetLines.length; x++) { + context.editor._codeMirror.indentLine(line + x); + } + + // give focus back + EditorManager.focusEditor(); + } + + // expects snippet, snippetTrigger, snippetArgs in context + function _executeSnippet(context) { + // load snippet from file if required + return _loadSnippetTemplate(context.snippet.template).done(function (template) { + context.template = template; + // prefill standard variables + context.template = SnippetPresets.execute(context.template); + // we need to find out if our snippet needs any more variables to fill + context.templateVariables = _getVariablesFromTemplate(context.template); + + var variablesDifference = context.snippetArgs.length - context.templateVariables.length; + if (variablesDifference > 0) { + // we have more variables than we require so we need to drop excess + if (context.snippetTrigger.str) { + // if we have trigger string we want variables right of it + context.snippetArgs = context.snippetArgs.slice(0, context.templateVariables.length); + } else { + // if we don't have trigger string we want variables before cursor position + context.snippetArgs = context.snippetArgs.slice(context.snippetArgs.length - context.templateVariables.length); + } + } + + if (variablesDifference >= 0) { + // we can now fill all our variables without the need for insert form + context.templateVariables.forEach(function (variable, index) { + // even my escapes have escapes + var re = new RegExp(variable.replace("$${", "\\$\\$\\{").replace("}", "\\}"), "g"); + context.template = context.template.replace(re, context.snippetArgs[index].str); + }); + return _insertSnippet(context); + } else { + // snippet form has to be triggered + + // we are not using any variables from line + context.snippetArgs = []; + + return _getInlineSnippetForm(context).done(function (inlineWidget) { + // decite a position for the widget + var newPos = { + line: context.pos.line - 1, + ch: context.pos.ch + }; + + context.editor.addInlineWidget(newPos, inlineWidget); + + var inlineComplete = function () { + context.templateVariables.forEach(function (variable) { + var re = new RegExp(variable.replace("$${", "\\$\\$\\{").replace("}", "\\}"), "g"), + variableNormalized = variable.replace("$${", "").replace("}", ""), + variableValue = inlineWidget.$form.find(".snipvar-" + variableNormalized).val(); + context.template = context.template.replace(re, variableValue); + }); + return _insertSnippet(context); + }; + + inlineWidget.$form.on("complete", function () { + inlineComplete(); + inlineWidget.close(); + }); + + }).fail(function (err) { + console.error(err); + }); + } + }); + } + + // triggers passed snippet on current cursor position + function triggerSnippet(snippet) { + var context = _createContext(); + context.snippet = snippet; + context.snippetTrigger = { + str: "", + ch: context.pos.ch + }; + // we need to get arguments before cursor position and fill snippetArgs + var props = _parseArgs(context.line); + context.snippetArgs = _.filter(props, function (prop) { return prop.ch <= context.pos.ch; }); + return _executeSnippet(context); + } + + // parses snippet from current line and triggers it + function triggerSnippetOnLine() { + var context = _createContext(); + + // find arguments on the line to identify a snippet to trigger + var props = _parseArgs(context.line); + if (props.length === 0) { return; } + + // try to find the snippet, given the trigger text + var triggers = _.pluck(snippets, "trigger"); + + // go in backwards order for a case there is an inline snippet along the way + for (var i = props.length - 1; i > 0; i--) { + + var io, + requireInline = true; + + // launch non-inline in inline mode snippets when %trigger is found + if (props[i].str[0] === "%") { + requireInline = false; + io = triggers.indexOf(props[i].str.substring(1)); + } else { + io = triggers.indexOf(props[i].str); + } + + if (io !== -1 && (snippets[io].inline || !requireInline)) { + // found inline snippet + context.snippet = snippets[io]; + context.snippetTrigger = props[i]; + context.snippetArgs = props.slice(i + 1); + return _executeSnippet(context); + } + } + + //no inline snippet found so look for any snippet that matches props[0] + //it can also have % as a prefix so remove + var lookFor = props[0].str; + if (lookFor[0] === "%") { + lookFor = lookFor.substring(1); + } + var snippet = _.find(snippets, function (s) { + return s.trigger === lookFor; + }); + if (snippet) { + context.snippet = snippet; + context.snippetTrigger = props[0]; + context.snippetArgs = props.slice(1); + return _executeSnippet(context); + } + } + + function init(_snippets) { + // fill module variable + snippets = _snippets; + //add the keybinding + var SNIPPET_EXECUTE_CMD = "snippets.execute"; + CommandManager.register("Run Snippet", SNIPPET_EXECUTE_CMD, triggerSnippetOnLine); + KeyBindingManager.addBinding(SNIPPET_EXECUTE_CMD, Preferences.get("triggerSnippetShortcut")); + } + + // Public API + exports.triggerSnippet = triggerSnippet; + exports.triggerSnippetOnLine = triggerSnippetOnLine; + exports.init = init; +}); diff --git a/src/SnippetPanel.js b/src/SnippetPanel.js new file mode 100644 index 0000000..314a22c --- /dev/null +++ b/src/SnippetPanel.js @@ -0,0 +1,85 @@ +define(function (require, exports) { + "use strict"; + + // Brackets modules + var CommandManager = brackets.getModule("command/CommandManager"), + Commands = brackets.getModule("command/Commands"), + EditorManager = brackets.getModule("editor/EditorManager"), + Menus = brackets.getModule("command/Menus"), + PanelManager = brackets.getModule("view/PanelManager"); + + // Dependencies + var Main = require("main"), + Preferences = require("src/Preferences"), + SettingsDialog = require("src/SettingsDialog"), + panelHtml = require("text!templates/bottom-panel.html"), + snippetsHTML = require("text!templates/snippets-table.html"); + + // Module constants + var SNIPPET_EXECUTE = "snippets.execute", + VIEW_HIDE_SNIPPETS = "snippets.hideSnippets"; + + // Module variables + var panel, + $panel; + + function renderTable(snippets) { + // render snippets table + var $snippetsTable = Mustache.render(snippetsHTML, {"snippets" : snippets}); + $panel.find(".resizable-content").append($snippetsTable); + $panel.find(".snippets-trigger").on("click", function () { + CommandManager.execute(SNIPPET_EXECUTE, [$(this).attr("data-trigger")]); + }); + $panel.find(".snippets-source").on("click", function () { + CommandManager.execute(Commands.FILE_OPEN, { fullPath: Main.getSnippetsDirectory() + "/" + $(this).attr("data-source") }); + }); + } + + function toggleSnippetPanel() { + var isVisible = !panel.isVisible(); + + if (isVisible) { + panel.show(); + } else { + panel.hide(); + } + + CommandManager.get(VIEW_HIDE_SNIPPETS).setChecked(isVisible); + EditorManager.resizeEditor(); + } + + function init() { + var s = Mustache.render(panelHtml); + panel = PanelManager.createBottomPanel(VIEW_HIDE_SNIPPETS, $(s), 100); + panel.hide(); + + $panel = $("#snippets"); + $panel + .on("click", ".snippets-settings", function () { + SettingsDialog.show(); + }) + .on("click", ".snippets-close", function () { + CommandManager.execute(VIEW_HIDE_SNIPPETS); + }); + + // Add toolbar icon + $("
") + .attr({ + id: "snippets-enable-icon", + href: "#" + }) + .click(toggleSnippetPanel) + .appendTo($("#main-toolbar .buttons")); + + // Add menu entry + var menu = Menus.getMenu(Menus.AppMenuBar.VIEW_MENU); + menu.addMenuItem(VIEW_HIDE_SNIPPETS, Preferences.get("showSnippetsPanelShortcut"), Menus.AFTER, Commands.VIEW_HIDE_SIDEBAR); + } + + // Register command + CommandManager.register("Show Snippets", VIEW_HIDE_SNIPPETS, toggleSnippetPanel); + + // Public API + exports.renderTable = renderTable; + exports.init = init; +}); diff --git a/src/SnippetPresets.js b/src/SnippetPresets.js index af62bfc..8015c0f 100644 --- a/src/SnippetPresets.js +++ b/src/SnippetPresets.js @@ -1,35 +1,13 @@ -/* - * Copyright (c) 2013 Jonathan Rowny - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - * DEALINGS IN THE SOFTWARE. - * - */ - -/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ -/*global define, brackets */ - define(function (require, exports) { - 'use strict'; + "use strict"; + + // Brackets modules var DocumentManager = brackets.getModule("document/DocumentManager"); - require("./date-format"); + // Dependencies + require("thirdparty/date-format"); + // Module variables var now, doc, presets = { @@ -116,6 +94,8 @@ define(function (require, exports) { } return input; } + + // Public API exports.execute = _execute; }); diff --git a/strings.js b/strings.js index 009acf0..603a3df 100644 --- a/strings.js +++ b/strings.js @@ -1,5 +1,3 @@ -/*global define */ - /** * This file provides the interface to user visible strings in Brackets. Code that needs * to display strings should should load this module by calling var Strings = require("strings"). diff --git a/templates/snippets-table.html b/templates/snippets-table.html index c93a65f..e7b80f4 100644 --- a/templates/snippets-table.html +++ b/templates/snippets-table.html @@ -4,6 +4,7 @@ Description Trigger Usage + Shortcut Source {{#snippets}} @@ -12,7 +13,8 @@ {{description}} {{trigger}} {{usage}} + {{shortcut}} {{source}} {{/snippets}} - \ No newline at end of file + diff --git a/src/date-format.js b/thirdparty/date-format.js similarity index 92% rename from src/date-format.js rename to thirdparty/date-format.js index 4ef392b..fc54df1 100644 --- a/src/date-format.js +++ b/thirdparty/date-format.js @@ -12,14 +12,14 @@ * The mask defaults to dateFormat.masks.default. */ -var dateFormat = function () { +var dateFormat = (function () { var token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g, timezone = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g, timezoneClip = /[^-+\dA-Z]/g, pad = function (val, len) { val = String(val); len = len || 2; - while (val.length < len) val = "0" + val; + while (val.length < len) { val = "0" + val; } return val; }; @@ -34,8 +34,8 @@ var dateFormat = function () { } // Passing date through Date applies Date.parse, if necessary - date = date ? new Date(date) : new Date; - if (isNaN(date)) throw SyntaxError("invalid date"); + date = date ? new Date(date) : new Date(); + if (isNaN(date)) { throw new SyntaxError("invalid date"); } mask = String(dF.masks[mask] || mask || dF.masks["default"]); @@ -89,7 +89,7 @@ var dateFormat = function () { return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1); }); }; -}(); +}()); // Some common format strings dateFormat.masks = { @@ -120,6 +120,9 @@ dateFormat.i18n = { }; // For convenience... +if (Date.prototype.format) { + throw new Error("Date.prototype.format is already defined!"); +} Date.prototype.format = function (mask, utc) { return dateFormat(this, mask, utc); -}; \ No newline at end of file +};