├── README.md ├── RegexUtil.js ├── main.js ├── package.json ├── regex-editor-styles.css ├── regex-editor-template.html └── regex-mode.js /README.md: -------------------------------------------------------------------------------- 1 | Inline Regex Editor for Brackets 2 | ================================ 3 | For those times when you need to [swoop in and save the day with a regular expression](https://xkcd.com/208/). 4 | 5 | Just put your cursor on a JavaScript regular expression and press Ctrl+E to bring up the editor. Enter test strings and see matches 6 | in real time as you edit the regexp. 7 | 8 | ![Screenshot](http://peterflynn.github.io/screenshots/brackets-regex-editor.png) 9 | 10 | * Displays regular expressions with full colored syntax highlighting 11 | * Highlights matched text in your test string 12 | * Shows all capturing-group matches 13 | * Mouseover a capturing-group match to highlight the corresponding group in the regex, and the match in your test string 14 | * Highlights matching parentheses (intelligently ignoring escaped characters) based on cursor position 15 | 16 | [Regular expressions can get pretty complicated](http://ex-parrot.com/~pdw/Mail-RFC822-Address.html) - that's why it's important to 17 | have good tools! 18 | 19 | 20 | How to Install 21 | ============== 22 | Inline Regex Editor is an extension for [Brackets](https://github.com/adobe/brackets/), a new open-source code editor for the web. 23 | 24 | To install extensions: 25 | 26 | 1. Choose _File > Extension Manager_ and select the _Available_ tab 27 | 2. Search for this extension 28 | 3. Click _Install_! 29 | 30 | 31 | ### License 32 | MIT-licensed -- see `main.js` for details. 33 | 34 | ### Compatibility 35 | Brackets Sprint 23 or newer (Adobe Edge Code Preview 4 or newer). -------------------------------------------------------------------------------- /RegexUtil.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013 Peter Flynn 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a 5 | * copy of this software and associated documentation files (the "Software"), 6 | * to deal in the Software without restriction, including without limitation 7 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | * and/or sell copies of the Software, and to permit persons to whom the 9 | * Software is furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | * DEALINGS IN THE SOFTWARE. 21 | */ 22 | 23 | 24 | /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, regexp: true */ 25 | /*global define, brackets, CodeMirror */ 26 | 27 | /** 28 | * Utilities for working with regular expressions and their matches 29 | */ 30 | define(function (require, exports, module) { 31 | "use strict"; 32 | 33 | // Our own modules 34 | var mode = require("regex-mode").mode; 35 | 36 | 37 | /** 38 | * Run our tokenizer over the regex and return an array of informal "token" objects 39 | */ 40 | function regexTokens(regexText) { 41 | var tokens = []; 42 | var state = CodeMirror.startState(mode); 43 | var stream = new CodeMirror.StringStream(regexText); 44 | while (!stream.eol()) { 45 | var style = mode.token(stream, state); 46 | tokens.push({ startCh: stream.start, string: stream.current(), style: style, nestLevel: state.nesting.length }); 47 | stream.start = stream.pos; 48 | } 49 | return tokens; 50 | } 51 | 52 | /** 53 | * Returns the range in regexText that represents capturing group #groupI. 54 | * Tokens are returned for internal reuse with findGroupInMatch(). 55 | * @param {string} regexText 56 | * @param {number} groupI 1-indexed 57 | * @return {{start:number, end:number, tokens:Array, startTokenI:number, endTokenI:number}?} 58 | * Null if no such group. 'end' is exclusive, but 'endTokenI' is inclusive. 59 | */ 60 | function findGroupInRegex(regexText, groupI) { 61 | 62 | var tokens = regexTokens(regexText); 63 | 64 | var startCh, startTokenI, nestLevel; 65 | 66 | // Find start of group 67 | var i, atGroupI = 0, token; 68 | for (i = 0; i < tokens.length; i++) { 69 | token = tokens[i]; 70 | if (token.style === "bracket" && token.string === "(") { 71 | if (!tokens[i + 1] || tokens[i + 1].string !== "?:") { // ignore non-capturing groups 72 | atGroupI++; 73 | token.groupI = atGroupI; // recorded for later use by findGroupInMatch() 74 | if (atGroupI === groupI) { 75 | startCh = token.startCh; 76 | startTokenI = i; 77 | nestLevel = token.nestLevel - 1; // nestLevel reflects post-token state, so we want 1 level up 78 | break; 79 | } 80 | } 81 | } 82 | } 83 | 84 | // Find end of group 85 | for ("ugh, jslint"; i < tokens.length; i++) { 86 | token = tokens[i]; 87 | if (token.nestLevel === nestLevel) { 88 | console.assert(token.style === "bracket" && token.string === ")"); 89 | return { start: startCh, end: token.startCh + token.string.length, tokens: tokens, 90 | startTokenI: startTokenI, endTokenI: i }; 91 | } 92 | } 93 | 94 | return null; 95 | } 96 | 97 | 98 | /** 99 | * Returns the range in sampleText that was matched by capturing group #groupI 100 | * @param {string} regexText 101 | * @param {string} options Options to pass to RegExp constructor 102 | * @param {string} sampleText 103 | * @param {Object} match 104 | * @param {number} groupI 1-indexed 105 | * @param {{start:number, end:number, tokens:Array}} groupPos Result of findGroupInRegex() 106 | * @return {?{start:number, end:number}} Null if no such group or group has no match. 'end' is exclusive. 107 | */ 108 | function findGroupInMatch(regexText, options, sampleText, match, groupI, groupPos) { 109 | if (!match || !groupPos || !match[groupI]) { 110 | return null; 111 | } 112 | 113 | var tokens = groupPos.tokens; 114 | 115 | // We don't support groups with quantifiers yet - only the last match is captured, and we don't yet have a way to figure 116 | // out the length of the uncaptures repeated matches before that. 117 | var tokenAfterGroup = tokens[groupPos.endTokenI + 1]; 118 | if (tokenAfterGroup && tokenAfterGroup.style === "rangeinfo") { 119 | return null; 120 | } 121 | 122 | // JS regexp results don't tell you the index of each group, only the index of the match overall. 123 | // Strategy is to construct a new regexp where the entire prefix of the match before the target group is wrapped in a 124 | // single group, so its match length tells us the offset of the target group's match from the start of the overall match. 125 | // In cases where the target group is nested, we wrap each nesting level's prefix in a group and then sum those prefix 126 | // groups' lengths. 127 | 128 | // Work backwards (right to left) from target group: start with a prefix group open just to the left of it, and close 129 | // that group if we encounter a "(" that forces us to step out a level, opening a new one on the other side of the "(". 130 | var newRegexText = ")"; 131 | var nextNestLevel = groupPos.tokens[groupPos.startTokenI].nestLevel - 1; 132 | var prefixGroups = []; 133 | var lastGroupI = tokens[groupPos.startTokenI].groupI; 134 | var i; 135 | for (i = groupPos.startTokenI - 1; i >= 0; i--) { 136 | var token = tokens[i]; 137 | if (token.style === "bracket" && token.string === "(") { 138 | // Not every "(" we encounter requires terminating our prefix group - only those in the path up from the target 139 | // group's nest level to the top level. E.g. in "a(b(c)d(e)f(TARGET))", only the leftmost "(" causes a break in 140 | // prefix groups. 141 | if (token.nestLevel === nextNestLevel) { 142 | nextNestLevel--; 143 | newRegexText = ")((" + newRegexText; 144 | // prefixGroupInsertions.push(i); // group inserted after tokens[i] in the original regexp's tokenization 145 | 146 | // This will be the index of our newly inserted group ONLY if we don't insert any more 147 | // new groups to the left of it. We'll correct for that later. 148 | prefixGroups.unshift(lastGroupI); 149 | } else { 150 | newRegexText = token.string + newRegexText; 151 | } 152 | 153 | // Update lastGroupI *after* the test above - the extra "(" we insert there is *inside* the "(" that our loop is 154 | // currently at, so we want the group number after it, not its group number. (And we can't just use `token.groupI + 1` 155 | // instead of tracking 'lastGroupI' since it's possible the "(" here is for a non-capturing group with no number; 156 | // the "(" token we're on may have no indication of what the next/prev group numbers are). 157 | if (token.groupI) { // no group # if non-capturing 158 | lastGroupI = token.groupI; 159 | } 160 | } else { 161 | newRegexText = token.string + newRegexText; 162 | } 163 | } 164 | // Close final outermost/leftmost prefix group 165 | newRegexText = "(" + newRegexText + regexText.substr(groupPos.start); 166 | prefixGroups.unshift(lastGroupI); 167 | console.assert(lastGroupI === 1); 168 | 169 | // console.log("NewRegexText:", newRegexText); 170 | 171 | // Adjust each prefix group's number to account for other prefix groups preceding it 172 | for (i = 1; i < prefixGroups.length; i++) { 173 | prefixGroups[i] += i; 174 | } 175 | // console.log("PrefixGroups:", prefixGroups); 176 | 177 | var newRegex = new RegExp(newRegexText, options); 178 | var newMatch = newRegex.exec(sampleText); 179 | // console.log("NewRegex:", newRegexText, "->", newMatch); 180 | 181 | // Sum the lengths of all our prefix groups' matching strings to get the offset of the target group's match 182 | var offsetFromMatchStart = 0; 183 | for (i = 0; i < prefixGroups.length; i++) { 184 | var groupMatch = newMatch[prefixGroups[i]]; 185 | if (groupMatch) { 186 | offsetFromMatchStart += groupMatch.length; 187 | } 188 | } 189 | var start = newMatch.index + offsetFromMatchStart; 190 | // console.log("Offset:", offsetFromMatchStart, "-> Start:", start, "End:", (start + match[groupI].length)); 191 | return { start: start, end: start + match[groupI].length }; 192 | } 193 | 194 | 195 | exports.findGroupInRegex = findGroupInRegex; 196 | exports.findGroupInMatch = findGroupInMatch; 197 | }); -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013 Peter Flynn 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a 5 | * copy of this software and associated documentation files (the "Software"), 6 | * to deal in the Software without restriction, including without limitation 7 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | * and/or sell copies of the Software, and to permit persons to whom the 9 | * Software is furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | * DEALINGS IN THE SOFTWARE. 21 | */ 22 | 23 | 24 | /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, regexp: true */ 25 | /*global define, brackets, $, CodeMirror */ 26 | 27 | /** 28 | * "Some people, when confronted with a problem, think 'I know, I'll use regular expressions!' Now they have two problems." 29 | * - Jamie Zawinski 30 | */ 31 | define(function (require, exports, module) { 32 | "use strict"; 33 | 34 | // Brackets modules 35 | var _ = brackets.getModule("thirdparty/lodash"), 36 | KeyEvent = brackets.getModule("utils/KeyEvent"), 37 | ExtensionUtils = brackets.getModule("utils/ExtensionUtils"), 38 | InlineWidget = brackets.getModule("editor/InlineWidget").InlineWidget, 39 | EditorManager = brackets.getModule("editor/EditorManager"), 40 | CodeMirror = brackets.getModule("thirdparty/CodeMirror2/lib/codemirror"); 41 | 42 | // Our own modules 43 | var mode = require("regex-mode").mode, 44 | RegexUtil = require("RegexUtil"); 45 | 46 | // UI templates 47 | var inlineEditorTemplate = require("text!regex-editor-template.html"); 48 | 49 | var TEST = /<.+>/; 50 | var TEST2 = /(a|x)(b|y)(c|z)/i; 51 | 52 | function findRegexToken(editor, pos) { 53 | var token = editor._codeMirror.getTokenAt(pos, true); // token to LEFT of cursor 54 | if (token.type === "string-2") { 55 | return token; 56 | } 57 | 58 | token = editor._codeMirror.getTokenAt({line: pos.line, ch: pos.ch + 1}, true); // token to RIGHT of cursor 59 | if (token.type === "string-2") { 60 | return token; 61 | } 62 | 63 | return null; 64 | } 65 | 66 | 67 | function RegexInlineEditor(regex, pos) { 68 | InlineWidget.call(this); 69 | this._origPos = pos; 70 | 71 | this.$htmlContent.addClass("inline-regex-editor"); 72 | $(inlineEditorTemplate).appendTo(this.$htmlContent); 73 | 74 | // Strip off the slash delimiters and separate out any flags 75 | var reInfo = regex.match(/^\/(.*)\/([igm]*)$/); 76 | if (!reInfo) { 77 | this._showError("Not a regular expression"); 78 | reInfo = [regex, regex, ""]; 79 | } 80 | this._origText = reInfo[1]; 81 | 82 | var $btnInsensitive = this.$htmlContent.find(".btn-regexp-insensitive"); 83 | $btnInsensitive 84 | .toggleClass("active", reInfo[2].indexOf("i") !== -1) 85 | .click(function () { 86 | $btnInsensitive.toggleClass("active"); 87 | this._handleChange(); 88 | }.bind(this)); 89 | 90 | var $inputField = this.$htmlContent.find(".inline-regex-edit"); 91 | $inputField.val(reInfo[1]); 92 | this.cm = CodeMirror.fromTextArea($inputField[0], { 93 | mode: "regex", 94 | matchBrackets: true, 95 | lineNumbers: false 96 | }); 97 | // The CM editor has "auto" height; in practice, it's only ever 1 line tall but height still grows if h scrollbar shown. 98 | // Its width is determined by its parent layout (a flexbox for now). 99 | 100 | this._handleChange = this._handleChange.bind(this); 101 | this.cm.on("change", this._handleChange); 102 | 103 | this.$sampleInput = this.$htmlContent.find(".inline-regex-sample") 104 | .on("input", this._handleChange); 105 | 106 | this._syncToCode = this._syncToCode.bind(this); 107 | this.$btnDone = this.$htmlContent.find(".btn-regexp-done") 108 | .click(this._syncToCode) 109 | .addClass("disabled"); // enabled when modified 110 | 111 | this.$htmlContent[0].addEventListener("keydown", this._handleKeyDown.bind(this), true); 112 | 113 | this.$htmlContent.find(".inline-regex-groups").on("mouseenter", ".regex-group-match", this._handleGroupMouseover.bind(this)); 114 | this.$htmlContent.find(".inline-regex-groups").on("mouseleave", ".regex-group-match", this._overlayFullMatch.bind(this)); 115 | } 116 | RegexInlineEditor.prototype = Object.create(InlineWidget.prototype); 117 | RegexInlineEditor.prototype.constructor = RegexInlineEditor; 118 | RegexInlineEditor.prototype.parentClass = InlineWidget.prototype; 119 | 120 | RegexInlineEditor.prototype.onAdded = function () { 121 | RegexInlineEditor.prototype.parentClass.onAdded.apply(this, arguments); 122 | 123 | this.cm.refresh(); // must refresh CM after it's initially added to DOM 124 | 125 | // Setting initial height is a *required* part of the InlineWidget contract 126 | // (must be after cm.refresh() so computed vertical size is accurate) 127 | this._adjustHeight(); 128 | 129 | this.cm.focus(); 130 | 131 | this._handleChange(); // initial update to show any syntax errors 132 | }; 133 | RegexInlineEditor.prototype._adjustHeight = function () { 134 | var inlineWidgetHeight = this.$htmlContent.find(".inline-regex-output-row").position().top + 28; 135 | this.hostEditor.setInlineWidgetHeight(this, inlineWidgetHeight); 136 | }; 137 | 138 | 139 | RegexInlineEditor.prototype._handleKeyDown = function (event) { 140 | if (event.keyCode === KeyEvent.DOM_VK_TAB) { 141 | // Create a closed tab cycle within the widget 142 | if (this.cm.hasFocus()) { 143 | this.$sampleInput.focus(); 144 | event.stopPropagation(); // don't insert tab in CM field 145 | } else { 146 | this.cm.focus(); 147 | } 148 | event.preventDefault(); 149 | 150 | } else if (event.keyCode === KeyEvent.DOM_VK_RETURN) { 151 | if (this.cm.hasFocus()) { 152 | event.stopPropagation(); // don't insert newline in CM field 153 | event.preventDefault(); 154 | // TODO: not thorough enough... could still paste, etc. 155 | } 156 | } 157 | }; 158 | 159 | RegexInlineEditor.prototype._showError = function (message) { 160 | this.$htmlContent.find(".inline-regex-error").text(message); 161 | this.$htmlContent.find(".inline-regex-error").show(); 162 | this.$htmlContent.find(".inline-regex-match").hide(); 163 | this.$htmlContent.find(".inline-regex-groups").hide(); 164 | this.$htmlContent.find(".sample-match-overlay").hide(); 165 | }; 166 | RegexInlineEditor.prototype._showNoMatch = function (testText) { 167 | if (!testText && !this._testTextModified) { 168 | // Don't show "no match" message if no test text ever entered before (but if it has been touched 169 | // and now is blank again, we assume user really wants to know if the regex matches empty string) 170 | this.$htmlContent.find(".inline-regex-error").hide(); 171 | } else { 172 | this.$htmlContent.find(".inline-regex-error").text("(no match)"); 173 | this.$htmlContent.find(".inline-regex-error").show(); 174 | if (testText) { 175 | this._testTextModified = true; 176 | } 177 | } 178 | this.$htmlContent.find(".inline-regex-match").hide(); 179 | this.$htmlContent.find(".inline-regex-groups").hide(); 180 | this.$htmlContent.find(".sample-match-overlay").hide(); 181 | }; 182 | RegexInlineEditor.prototype._showMatch = function () { 183 | var match = this._match; 184 | var padding = "", i; 185 | for (i = 0; i < match.index; i++) { padding += " "; } 186 | this.$htmlContent.find(".inline-regex-match").html(padding + _.escape(match[0])); 187 | 188 | // Show capturing group matches 189 | if (match.length > 1) { 190 | var groups = ""; 191 | for (i = 1; i < match.length; i++) { 192 | if (match[i] !== undefined) { 193 | groups += "$" + i + " "; 194 | groups += _.escape(match[i]); 195 | groups += ""; 196 | } else { 197 | groups += "$" + i + ""; 198 | } 199 | } 200 | this.$htmlContent.find(".inline-regex-groups").html(groups).show(); 201 | } else { 202 | this.$htmlContent.find(".inline-regex-groups").hide(); 203 | } 204 | 205 | this.$htmlContent.find(".inline-regex-match").show(); 206 | this.$htmlContent.find(".inline-regex-error").hide(); 207 | this._overlayFullMatch(); 208 | }; 209 | 210 | RegexInlineEditor.prototype._getFlags = function () { 211 | return this.$htmlContent.find(".btn-regexp-insensitive").hasClass("active") ? "i" : ""; 212 | }; 213 | 214 | RegexInlineEditor.prototype._highlightSampleText = function (start, len) { 215 | var $overlay = this.$htmlContent.find(".sample-match-overlay").show(); 216 | $overlay.css("left", 47 + 7 * start); 217 | $overlay.css("width", 7 * len); 218 | $overlay.css("top", this.$sampleInput.position().top); 219 | }; 220 | RegexInlineEditor.prototype._overlayFullMatch = function () { 221 | this._highlightSampleText(this._match.index, this._match[0].length); 222 | 223 | if (this._regexGroupHighlight) { 224 | this._regexGroupHighlight.clear(); 225 | this._regexGroupHighlight = null; 226 | } 227 | }; 228 | RegexInlineEditor.prototype._handleGroupMouseover = function (event) { 229 | var regexText = this.cm.getValue(); 230 | var testText = this.$sampleInput.val(); 231 | var groupI = parseInt($(event.currentTarget).data("groupnum"), 10); 232 | 233 | var inRegex = RegexUtil.findGroupInRegex(regexText, groupI); 234 | this._regexGroupHighlight = this.cm.markText({line: 0, ch: inRegex.start}, {line: 0, ch: inRegex.end}, 235 | {className: "regex-group-highlight", startStyle: "rgh-first", endStyle: "rgh-last" }); 236 | 237 | var inSample = RegexUtil.findGroupInMatch(regexText, this._getFlags(), testText, this._match, groupI, inRegex); 238 | if (inSample) { 239 | this._highlightSampleText(inSample.start, inSample.end - inSample.start); 240 | } else { 241 | this.$htmlContent.find(".sample-match-overlay").hide(); 242 | } 243 | }; 244 | 245 | RegexInlineEditor.prototype._handleChange = function () { 246 | var regexText = this.cm.getValue(); 247 | var testText = this.$sampleInput.val(); 248 | 249 | this.$btnDone.toggleClass("disabled", this._origText === regexText); 250 | 251 | if (!regexText) { 252 | this._showError("Empty regular expression is not valid"); 253 | } else { 254 | var regex; 255 | try { 256 | regex = new RegExp(regexText, this._getFlags()); 257 | } catch (ex) { 258 | this._showError(ex.message); 259 | return; 260 | } 261 | 262 | this._match = regex.exec(testText); 263 | if (!this._match) { 264 | this._showNoMatch(testText); 265 | } else { 266 | this._showMatch(); 267 | } 268 | } 269 | 270 | this._adjustHeight(); // in case CM h scrollbar added/removed 271 | }; 272 | 273 | RegexInlineEditor.prototype._syncToCode = function () { 274 | var regexText = this.cm.getValue(); 275 | var regexCode = "/" + regexText + "/" + this._getFlags(); 276 | 277 | // Rough way to re-find orig token... hopefully dependable enough 278 | var token = findRegexToken(this.hostEditor, this._origPos); 279 | 280 | var chStart = token ? token.start : this._origPos.ch, 281 | len = token ? token.string.length : 0; 282 | this.hostEditor.document.replaceRange(regexCode, { line: this._origPos.line, ch: chStart }, { line: this._origPos.line, ch: chStart + len }); 283 | 284 | this.close(); 285 | }; 286 | 287 | 288 | /** 289 | * @param {!Editor} hostEditor 290 | */ 291 | function _createInlineEditor(hostEditor, pos, regexToken) { 292 | var inlineEditor = new RegexInlineEditor(regexToken.string, pos); 293 | inlineEditor.load(hostEditor); // only needed to appease weird InlineWidget API 294 | 295 | return new $.Deferred().resolve(inlineEditor); 296 | } 297 | 298 | /** 299 | * Provider registered with EditorManager 300 | * 301 | * @param {!Editor} editor 302 | * @param {!{line:Number, ch:Number}} pos 303 | * @return {?$.Promise} Promise resolved with a RegexInlineEditor, or null 304 | */ 305 | function javaScriptFunctionProvider(hostEditor, pos) { 306 | // Only provide a JavaScript editor when cursor is in JavaScript content 307 | if (hostEditor.getLanguageForSelection().getId() !== "javascript") { 308 | return null; 309 | } 310 | 311 | var token = findRegexToken(hostEditor, pos); 312 | if (token) { 313 | return _createInlineEditor(hostEditor, pos, token); 314 | } 315 | return null; 316 | } 317 | 318 | 319 | ExtensionUtils.loadStyleSheet(module, "regex-editor-styles.css"); 320 | 321 | EditorManager.registerInlineEditProvider(javaScriptFunctionProvider); 322 | }); 323 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pflynn.regex-editor", 3 | "title": "Inline Regex Editor", 4 | "homepage": "https://github.com/peterflynn/brackets-regex-editor", 5 | "author": "Peter Flynn", 6 | "version": "1.1.3", 7 | "engines": { "brackets": ">=0.38" }, 8 | "description": "Inline regular expression editor for JavaScript code: enter test strings and see matches in real time as you edit the regexp - with full syntax highlighting and paren/brace matching. Mouseover capturing group matches to highlight the group within the regexp and the text within the test string." 9 | } -------------------------------------------------------------------------------- /regex-editor-styles.css: -------------------------------------------------------------------------------- 1 | .inline-regex-regex-row, .inline-regex-sample-row { 2 | display: flex; 3 | flex-direction: row; 4 | align-items: center; 5 | width: 100%; 6 | box-sizing: border-box; 7 | padding-left: 40px; 8 | padding-right: 45px; 9 | } 10 | .inline-regex-editor .CodeMirror, .inline-regex-sample { 11 | flex: 1 1 auto; /* fully flexible */ 12 | } 13 | .inline-regex-regex-row > button { 14 | flex: 0 0 auto; /* inflexible (natural size only, never larger/smaller) */ 15 | } 16 | 17 | .inline-regex-editor .CodeMirror { 18 | margin: 8px 0; 19 | 20 | /* imitating appearance */ 21 | background: white; 22 | border: 1px solid #9c9e9e; 23 | border-radius: 3px; 24 | box-shadow: inset 0 1px 0 rgba(0,0,0,0.12); 25 | } 26 | .inline-regex-editor .CodeMirror .cm-keyword, .inline-regex-editor .CodeMirror .cm-bracket, .inline-regex-editor .CodeMirror .cm-rangeinfo { 27 | font-weight: bold; 28 | } 29 | .inline-regex-editor .CodeMirror .cm-bracket { 30 | color: #535353; 31 | } 32 | 33 | .inline-regex-editor .CodeMirror-focused { 34 | /* imitating focused appearance - but with toned-down focus ring so syntax coloring easier to see */ 35 | border: 1px solid rgba(9, 64, 253, 0.5); 36 | box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(111, 181, 241, 0.38); 37 | } 38 | .inline-regex-editor input[type='text']:focus { 39 | border: 1px solid rgba(9, 64, 253, 0.5); 40 | box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(111, 181, 241, 0.38); 41 | } 42 | 43 | .inline-regex-editor .CodeMirror-lines { 44 | padding: 5px 0; 45 | } 46 | .inline-regex-editor .CodeMirror pre { 47 | padding: 0 6px; 48 | } 49 | .inline-regex-editor .CodeMirror .regex-group-highlight { 50 | background-color: rgba(123, 137, 217, 0.37); 51 | } 52 | .inline-regex-editor .CodeMirror .rgh-first { 53 | border-radius: 3px 0 0 3px; 54 | } 55 | .inline-regex-editor .CodeMirror .rgh-last { 56 | border-radius: 0 3px 3px 0; 57 | } 58 | 59 | .btn-regexp-insensitive, .btn-regexp-done { 60 | margin-left: 10px; 61 | } 62 | 63 | 64 | .inline-regex-editor .inline-regex-sample { 65 | font-family: "SourceCodePro"; 66 | font-size: inherit; 67 | padding: 2px 2px 2px 6px; /* 6px aligns L edge of text with CM field above */ 68 | } 69 | 70 | 71 | .inline-regex-editor .inline-regex-match { 72 | margin-left: 47px; 73 | font-family: "SourceCodePro"; 74 | font-size: inherit; 75 | color: #000090 76 | } 77 | 78 | .inline-regex-editor .inline-regex-groups { 79 | margin-left: 5px; 80 | background-color: #d3d3ff; 81 | border-radius: 4px; 82 | padding: 0 3px; 83 | font-family: "SourceCodePro"; 84 | font-size: inherit; 85 | color: #000090; 86 | } 87 | .inline-regex-editor .inline-regex-groups .regex-group-match { 88 | padding: 0 5px; 89 | } 90 | .inline-regex-editor .inline-regex-groups .regex-group-match.unmatched-group { 91 | opacity: 0.50; 92 | } 93 | .inline-regex-editor .inline-regex-groups .regex-group-match:hover { 94 | background-color: #9f9ff5; 95 | } 96 | .inline-regex-editor .inline-regex-groups .regex-group-match strong { 97 | font-weight: bold; 98 | } 99 | 100 | .inline-regex-editor .inline-regex-error { 101 | margin-left: 40px; 102 | font-family: "SourceSansPro"; 103 | color: #be0000; 104 | } 105 | 106 | .inline-regex-editor .sample-match-overlay { 107 | position: absolute; 108 | pointer-events: none; 109 | background-color: rgba(123, 137, 217, 0.37); 110 | border-radius: 2px; 111 | height: 18px; 112 | 113 | /* top is programmatically set to match top of .inline-regex-sample; adjust downward to match its border + padding */ 114 | margin-top: 3px; 115 | } -------------------------------------------------------------------------------- /regex-editor-template.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 |
7 | 8 |
9 |
10 | 11 | 12 | 13 |
14 | -------------------------------------------------------------------------------- /regex-mode.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013 Peter Flynn. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a 5 | * copy of this software and associated documentation files (the "Software"), 6 | * to deal in the Software without restriction, including without limitation 7 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | * and/or sell copies of the Software, and to permit persons to whom the 9 | * Software is furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | * DEALINGS IN THE SOFTWARE. 21 | */ 22 | 23 | 24 | /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, regexp: true */ 25 | /*global define, brackets, CodeMirror */ 26 | 27 | /** 28 | * A CodeMirror mode for syntax-highlighting regular expressions, mapped to .regex file extension. 29 | * Each line is assumed to be a separate regex. 30 | * 31 | * The fact that escaped "()"s are a different token class from unescaped ones means CodeMirror's 32 | * standard brace matching will work correctly with this mode as well. 33 | */ 34 | define(function (require, exports, module) { 35 | "use strict"; 36 | 37 | // Brackets modules 38 | var LanguageManager = brackets.getModule("language/LanguageManager"), 39 | CodeMirror = brackets.getModule("thirdparty/CodeMirror2/lib/codemirror"); 40 | 41 | 42 | function clearState(state) { 43 | state.nesting = []; 44 | state.quantifiable = false; 45 | state.justOpened = null; 46 | return state; 47 | } 48 | function startState() { 49 | return clearState({}); 50 | } 51 | 52 | 53 | var charClasses_arr = ["d", "D", "w", "W", "s", "S", "t", "r", "n", "v", "f", "b", "B", "0"]; 54 | var charClasses = {}; 55 | charClasses_arr.forEach(function (ch) { charClasses[ch] = true; }); 56 | 57 | 58 | function token(stream, state) { 59 | 60 | var newScope = state.justOpened; 61 | if (newScope) { 62 | state.justOpened = null; 63 | 64 | if (newScope === "(") { 65 | if (stream.match(/\?[:=!]/)) { 66 | // ?: is non-capturing qualifier 67 | // ?= and ?! are only-if-[not]-followed-by directives, technically making the whole group a quantifier 68 | state.quantifiable = false; 69 | return "keyword"; 70 | } 71 | } else if (newScope === "[") { 72 | if (stream.eat("^")) { // char set inversion 73 | state.quantifiable = false; 74 | return "keyword"; 75 | } 76 | } 77 | } 78 | 79 | 80 | var ch = stream.next(); 81 | 82 | // Reset for new regexp at start of next line 83 | if (!ch) { 84 | clearState(state); 85 | return; 86 | } 87 | 88 | 89 | function pushNest() { 90 | state.nesting.push(ch); 91 | state.justOpened = ch; 92 | } 93 | function popNest() { 94 | state.nesting.pop(); 95 | } 96 | 97 | 98 | if (ch === "\\") { 99 | state.quantifiable = true; 100 | 101 | var nextCh = stream.next(); 102 | if (charClasses[nextCh]) { 103 | return "atom"; 104 | } else if (nextCh === "u") { 105 | stream.next(); 106 | stream.next(); 107 | stream.next(); 108 | stream.next(); 109 | return "atom"; 110 | } else if (nextCh === "x") { 111 | stream.next(); 112 | stream.next(); 113 | return "atom"; 114 | } else if (!nextCh) { 115 | return "error"; // regexp cannot end in \ 116 | } else { 117 | return null; // just a random escaped character 118 | // TODO: render the "\" slightly grayed out? 119 | } 120 | // TODO: \n backreferences (n>0) 121 | } 122 | if (ch === ".") { 123 | state.quantifiable = true; 124 | return "atom"; 125 | } 126 | 127 | var scope = state.nesting[state.nesting.length - 1]; // may yield undefined 128 | 129 | if (scope === "[") { 130 | if (ch === "]") { 131 | popNest(); 132 | state.quantifiable = true; // overall char set can be quantified 133 | // (else: no need to set quantifiable=false otherwise, since quantifiers not accepted inside char clas anyway) 134 | } else if (ch === "-" && !newScope) { 135 | return "keyword"; // char range (unless 1st char after [) 136 | } 137 | 138 | return "number"; 139 | } 140 | 141 | if (ch === "(") { 142 | pushNest(); 143 | state.quantifiable = false; 144 | return "bracket"; 145 | } else if (ch === "[") { 146 | pushNest(); 147 | state.quantifiable = false; 148 | return "number"; 149 | } else if (ch === ")") { 150 | state.quantifiable = true; // overall group can be quantified 151 | if (scope === "(") { 152 | popNest(); 153 | return "bracket"; 154 | } else { 155 | return "error"; 156 | } 157 | } 158 | // Note: closing "]" floating outside a "[" context is fine - just plain text 159 | 160 | 161 | // Start-line/end-line marker 162 | // Theyre actually interpreted this way even if not at start/end of regexp... which does make sense sometimes, e.g. with | operator 163 | if (ch === "^") { 164 | state.quantifiable = false; 165 | return "keyword"; 166 | } 167 | if (ch === "$") { 168 | state.quantifiable = false; 169 | return "keyword"; 170 | } 171 | 172 | // Quantifiers 173 | function handleQuantifier() { 174 | if (state.quantifiable) { 175 | state.quantifiable = false; 176 | return "rangeinfo"; 177 | } else { 178 | return "error"; 179 | } 180 | } 181 | if (ch === "+" || ch === "*" || ch === "?") { 182 | stream.eat("?"); // +? or *? or ?? (non-greedy quantifier) 183 | return handleQuantifier(); 184 | } else if (ch === "{") { 185 | if (stream.match(/\d+(,\d*)?\}/)) { 186 | // TODO: turn red if n>m (ok if n==m though) 187 | return handleQuantifier(); 188 | } 189 | // Anything after "{" other than {n} {n,} or {n,m} makes it just plain text to match 190 | return null; 191 | } 192 | 193 | if (ch === "|") { 194 | state.quantifiable = false; 195 | return "keyword"; 196 | } 197 | 198 | // Any other plain text chars to match (e.g. letters or digits) 199 | state.quantifiable = true; 200 | return null; 201 | } 202 | 203 | 204 | var modeFactory = function () { 205 | return { token: token, startState: startState }; 206 | }; 207 | 208 | CodeMirror.defineMode("regex", modeFactory); 209 | 210 | exports.mode = modeFactory(); 211 | }); 212 | --------------------------------------------------------------------------------