├── .gitignore ├── images ├── icon.png └── in_action.gif ├── .github └── assignment.yml ├── .vscodeignore ├── tsconfig.json ├── .vscode ├── tasks.json ├── settings.json └── launch.json ├── yarn.lock ├── LICENSE ├── README.md ├── package.json └── src ├── chat.ts └── extension.ts /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | *.vsix -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrmarti/vscode-regex/HEAD/images/icon.png -------------------------------------------------------------------------------- /.github/assignment.yml: -------------------------------------------------------------------------------- 1 | { 2 | perform: true, 3 | assignees: [ chrmarti ] 4 | } 5 | -------------------------------------------------------------------------------- /images/in_action.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrmarti/vscode-regex/HEAD/images/in_action.gif -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .github/** 3 | out/test/** 4 | test/** 5 | src/** 6 | **/*.map 7 | .gitignore 8 | tsconfig.json 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2022", 5 | "lib": [ 6 | "ES2022" 7 | ], 8 | "outDir": "out", 9 | "sourceMap": true, 10 | "rootDir": "src", 11 | "strict": true 12 | }, 13 | "exclude": [ 14 | "node_modules" 15 | ] 16 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off" 11 | } -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@types/vscode@^1.92.0": 6 | version "1.93.0" 7 | resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.93.0.tgz#1cd7573e0272aef9c357bafc635b6177c154013e" 8 | integrity sha512-kUK6jAHSR5zY8ps42xuW89NLcBpw1kOabah7yv38J8MyiYuOHxLQBi0e7zeXbQgVefDy/mZZetqEFC+Fl5eIEQ== 9 | 10 | typescript@^5.6.2: 11 | version "5.6.2" 12 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.2.tgz#d1de67b6bef77c41823f822df8f0b3bcff60a5a0" 13 | integrity sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw== 14 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ], 15 | "outFiles": [ 16 | "${workspaceFolder}/dist/**/*.js" 17 | ], 18 | "preLaunchTask": "${defaultBuildTask}" 19 | }, 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Microsoft Corporation 2 | 3 | All rights reserved. 4 | 5 | MIT License 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation 8 | files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, 9 | modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 15 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 16 | BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT 17 | OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Features 2 | 3 | Shows the current regular expression's matches in a side-by-side document. This can be turned on/off with `Ctrl+Alt+M` (`⌥⌘M`). 4 | 5 | Global and multiline options can be added for evaluation with a side-by-side document through a status bar entry. This can be useful when the side-by-side document has multiple examples to match. 6 | 7 | ![Regex Previewer in Action](images/in_action.gif) 8 | 9 | ## Release Notes 10 | 11 | ### 0.6.0 12 | 13 | - Add support for `d` flag in TypeScript/JavaScript (PR by [@wszgrcy](https://github.com/wszgrcy)) 14 | 15 | ### 0.5.0 16 | 17 | - Add chat participant 18 | 19 | ### 0.4.0 20 | 21 | - Also publish as web extension 22 | 23 | ### 0.3.0 24 | 25 | - Add React support (PR by [@galangel](https://github.com/galangel)) 26 | - Add Vue support (PR by [@jombard](https://github.com/jombard)) 27 | - Code cleanup 28 | 29 | ### 0.2.0 30 | 31 | - Add PHP support (PR by [@ergunsh](https://github.com/ergunsh)) 32 | - Add setting to disable the code lens (PR by [@ergunsh](https://github.com/ergunsh)) 33 | - Add more sample text (PR by [@mscolnick](https://github.com/mscolnick)) 34 | 35 | ### 0.1.0 36 | 37 | - Status bar item to toggle adding global and multiline flags for evaluation with example text. 38 | - Bugfixes 39 | 40 | ### 0.0.8 41 | 42 | - Add Haxe support (PR by [@Gama11](https://github.com/Gama11)) 43 | 44 | ### 0.0.7 45 | 46 | - Several bugfixes 47 | - Catch up with latest release 48 | 49 | ### 0.0.6 50 | 51 | - Single toggle action to turn Regex matching on/off 52 | 53 | ### 0.0.5 54 | 55 | - Make it work on Windows again... 56 | 57 | ### 0.0.4 58 | 59 | - Allow any editor to show matches 60 | - Avoid storing sample file in installation directory 61 | - More bugfixes 62 | 63 | ### 0.0.3 64 | 65 | - More bugfixes, make it work on Windows. 66 | - Add icon 67 | 68 | ### 0.0.2 69 | 70 | Bugfixes. 71 | 72 | ### 0.0.1 73 | 74 | Initial release. 75 | 76 | ## License 77 | 78 | [MIT](LICENSE) 79 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "regex", 3 | "displayName": "Regex Previewer", 4 | "description": "Regex matches previewer for JavaScript, TypeScript, PHP and Haxe in Visual Studio Code.", 5 | "version": "0.6.0", 6 | "publisher": "chrmarti", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/chrmarti/vscode-regex.git" 10 | }, 11 | "license": "MIT", 12 | "bugs": { 13 | "url": "https://github.com/chrmarti/vscode-regex/issues" 14 | }, 15 | "keywords": [ 16 | "regex", 17 | "regular expression", 18 | "regexp", 19 | "test" 20 | ], 21 | "icon": "images/icon.png", 22 | "engines": { 23 | "vscode": "^1.92.0" 24 | }, 25 | "categories": [ 26 | "Other", 27 | "Chat", 28 | "AI" 29 | ], 30 | "activationEvents": [ 31 | "onLanguage:javascript", 32 | "onLanguage:javascriptreact", 33 | "onLanguage:typescript", 34 | "onLanguage:typescriptreact", 35 | "onLanguage:vue", 36 | "onLanguage:php", 37 | "onLanguage:haxe" 38 | ], 39 | "main": "./out/extension", 40 | "browser": "./out/extension", 41 | "contributes": { 42 | "configuration": { 43 | "type": "object", 44 | "title": "Regex Previewer Configuration", 45 | "properties": { 46 | "regex-previewer.enableCodeLens": { 47 | "scope": "resource", 48 | "type": "boolean", 49 | "default": true, 50 | "description": "Enables code lens for Regex Previewer" 51 | } 52 | } 53 | }, 54 | "commands": [ 55 | { 56 | "command": "extension.toggleRegexPreview", 57 | "title": "Toggle Regex Preview In Side-By-Side Editors" 58 | } 59 | ], 60 | "keybindings": [ 61 | { 62 | "command": "extension.toggleRegexPreview", 63 | "key": "ctrl+alt+m", 64 | "mac": "cmd+alt+m" 65 | } 66 | ], 67 | "chatParticipants": [ 68 | { 69 | "id": "regex.chatParticipant", 70 | "fullName": "Regex", 71 | "name": "regex", 72 | "description": "Talk to me about regexes!", 73 | "isSticky": true, 74 | "commands": [ 75 | { 76 | "name": "new", 77 | "description": "Create a new regex from a description", 78 | "disambiguation": [ 79 | { 80 | "category": "regex_new", 81 | "description": "The user wants to create a new regex from a description.", 82 | "examples": [ 83 | "Create a new regex matching something", 84 | "New regex matching something", 85 | "Build a regex for something" 86 | ] 87 | } 88 | ] 89 | }, 90 | { 91 | "name": "new-from", 92 | "description": "Create a new regex from a sample", 93 | "disambiguation": [ 94 | { 95 | "category": "regex_new-from", 96 | "description": "The user wants to create a new regex from a sample.", 97 | "examples": [ 98 | "Create a new regex that matches this", 99 | "New regex from sample", 100 | "Build a regex for this" 101 | ] 102 | } 103 | ] 104 | } 105 | ], 106 | "disambiguation": [ 107 | { 108 | "category": "regex", 109 | "description": "The user wants to understand a regex.", 110 | "examples": [ 111 | "Explain this regex", 112 | "What does this regex do?", 113 | "How does this regex work?" 114 | ] 115 | } 116 | ] 117 | } 118 | ] 119 | }, 120 | "scripts": { 121 | "vscode:prepublish": "yarn run compile", 122 | "compile": "tsc -p ./", 123 | "watch": "tsc -watch -p ./", 124 | "package": "vsce package", 125 | "publish": "vsce publish" 126 | }, 127 | "devDependencies": { 128 | "@types/vscode": "^1.92.0", 129 | "typescript": "^5.6.2" 130 | } 131 | } -------------------------------------------------------------------------------- /src/chat.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | import * as vscode from 'vscode'; 6 | 7 | const REGEX_PARTICIPANT_ID = 'regex.chatParticipant'; 8 | 9 | const MODEL_SELECTOR: vscode.LanguageModelChatSelector = { vendor: 'copilot', family: 'gpt-4o' }; 10 | 11 | export function registerChatParticipant(context: vscode.ExtensionContext) { 12 | 13 | const handler: vscode.ChatRequestHandler = async (request: vscode.ChatRequest, context: vscode.ChatContext, stream: vscode.ChatResponseStream, token: vscode.CancellationToken): Promise => { 14 | try { 15 | let [model] = await vscode.lm.selectChatModels(MODEL_SELECTOR); 16 | if (!model) { 17 | [model] = await vscode.lm.selectChatModels(); 18 | if (!model) { 19 | stream.markdown('No language model available.'); 20 | return; 21 | } 22 | } 23 | const messages = []; 24 | if (!request.command) { 25 | for (let i = context.history.length - 1; i >= 0; i--) { 26 | const turn = context.history[i]; 27 | if (turn.participant !== REGEX_PARTICIPANT_ID) { 28 | break; 29 | } 30 | if (turn instanceof vscode.ChatRequestTurn) { 31 | if (turn.command || i === 0 || context.history[i - 1].participant !== REGEX_PARTICIPANT_ID) { 32 | messages.push(...getInitialPrompt(turn).reverse()); 33 | break; 34 | } 35 | messages.push(vscode.LanguageModelChatMessage.User(turn.prompt)); 36 | } else if (turn instanceof vscode.ChatResponseTurn) { 37 | for (const part of turn.response) { 38 | if (part instanceof vscode.ChatResponseMarkdownPart) { 39 | messages.push(vscode.LanguageModelChatMessage.Assistant(part.value.value)); 40 | } 41 | } 42 | } 43 | } 44 | messages.reverse(); 45 | } 46 | if (!messages.length) { 47 | messages.push(...getInitialPrompt(request)); 48 | } else { 49 | messages.push(vscode.LanguageModelChatMessage.User(request.prompt)); 50 | } 51 | 52 | const chatResponse = await model.sendRequest(messages, {}, token); 53 | for await (const fragment of chatResponse.text) { 54 | stream.markdown(fragment); 55 | } 56 | } catch (err) { 57 | if (err instanceof vscode.LanguageModelError) { 58 | stream.markdown(`${err.message} (${err.code})`); 59 | } else { 60 | throw err; 61 | } 62 | } 63 | }; 64 | 65 | function getInitialPrompt({ prompt, command }: { prompt: string; command?: string }) { 66 | const languageId = vscode.window.activeTextEditor?.document.languageId 67 | || vscode.window.visibleTextEditors[0]?.document.languageId 68 | || vscode.workspace.textDocuments[0]?.languageId 69 | || 'javascript'; 70 | const languageName = languageIdToNameMapping[languageId] || languageId; 71 | 72 | if (command === 'new') { 73 | return [ 74 | vscode.LanguageModelChatMessage.User('You are an expert in regular expressions! Your job is to create regular expressions based on descriptions of what has to match.'), 75 | vscode.LanguageModelChatMessage.User(`Create a ${languageName} regular expression that matches text with the following description: ${prompt}`), 76 | ]; 77 | } 78 | if (command === 'new-from') { 79 | return [ 80 | vscode.LanguageModelChatMessage.User('You are an expert in regular expressions! Your job is to create regular expressions based on text samples that have to match.'), 81 | vscode.LanguageModelChatMessage.User(`Create a ${languageName} regular expression that matches the following samples: ${prompt}`), 82 | ]; 83 | } 84 | return [ 85 | vscode.LanguageModelChatMessage.User(`You are an expert in regular expressions! Your job is to assist with regular expressions in ${languageName}.`), 86 | vscode.LanguageModelChatMessage.User(prompt), 87 | ] 88 | } 89 | 90 | const regex = vscode.chat.createChatParticipant(REGEX_PARTICIPANT_ID, handler); 91 | regex.iconPath = vscode.Uri.joinPath(context.extensionUri, 'images', 'icon.png'); 92 | 93 | context.subscriptions.push( 94 | regex, 95 | ); 96 | } 97 | 98 | const languageIdToNameMapping: { [id: string]: string } = { 99 | 'javascript': 'JavaScript', 100 | 'javascriptreact': 'JavaScript React', 101 | 'typescript': 'TypeScript', 102 | 'typescriptreact': 'TypeScript React', 103 | 'vue': 'Vue', 104 | 'php': 'PHP', 105 | 'haxe': 'Haxe', 106 | 'python': 'Python', 107 | 'java': 'Java', 108 | 'csharp': 'C#', 109 | 'cpp': 'C++', 110 | 'ruby': 'Ruby', 111 | 'go': 'Go', 112 | 'rust': 'Rust', 113 | 'swift': 'Swift', 114 | 'kotlin': 'Kotlin', 115 | 'scala': 'Scala', 116 | 'perl': 'Perl', 117 | 'html': 'HTML', 118 | 'css': 'CSS', 119 | 'markdown': 'Markdown', 120 | 'json': 'JSON', 121 | 'xml': 'XML', 122 | 'sql': 'SQL', 123 | }; 124 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 'use strict'; 6 | 7 | import * as vscode from 'vscode'; 8 | import { registerChatParticipant } from './chat'; 9 | 10 | declare type IntervalToken = object; 11 | declare function setInterval(fn: () => void, delay: number): IntervalToken; 12 | declare function clearInterval(token: IntervalToken): void; 13 | 14 | export function activate(context: vscode.ExtensionContext) { 15 | registerChatParticipant(context); 16 | 17 | const regexRegex = /(^|\s|[()={},:?;])(\/((?:\\\/|\[[^\]]*\]|[^/])+)\/([gimuyd]*))(\s|[()={},:?;]|$)/g; 18 | const phpRegexRegex = /(^|\s|[()={},:?;])['|"](\/((?:\\\/|\[[^\]]*\]|[^/])+)\/([gimuy]*))['|"](\s|[()={},:?;]|$)/g; 19 | const haxeRegexRegex = /(^|\s|[()={},:?;])(~\/((?:\\\/|\[[^\]]*\]|[^/])+)\/([gimsu]*))(\s|[.()={},:?;]|$)/g; 20 | const regexHighlight = vscode.window.createTextEditorDecorationType({ backgroundColor: 'rgba(100,100,100,.35)' }); 21 | const matchHighlight = vscode.window.createTextEditorDecorationType({ backgroundColor: 'rgba(255,255,0,.35)' }); 22 | 23 | const matchesFileContent = `Lorem ipsum dolor sit amet, consectetur adipiscing elit, 24 | sed do eiusmod tempor incididunt ut labore et dolore magna 25 | aliqua. Ut enim ad minim veniam, quis nostrud exercitation 26 | ullamco laboris nisi ut aliquip ex ea commodo consequat. 27 | Duis aute irure dolor in reprehenderit in voluptate velit 28 | esse cillum dolore eu fugiat nulla pariatur. Excepteur sint 29 | occaecat cupidatat non proident, sunt in culpa qui officia 30 | deserunt mollit anim id est laborum. 31 | 32 | abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 33 | 0123456789 _+-.,!@#$%^&*();\/|<>"' 34 | 12345 -98.7 3.141 .6180 9,000 +42 35 | 555.123.4567 +1-(800)-555-2468 36 | foo@demo.net bar.ba@test.co.uk 37 | www.demo.com http://foo.co.uk/ 38 | https://marketplace.visualstudio.com/items?itemName=chrmarti.regex 39 | https://github.com/chrmarti/vscode-regex 40 | `; 41 | const languages = ['javascript', 'javascriptreact', 'typescript', 'typescriptreact', 'vue', 'php', 'haxe']; 42 | 43 | const decorators = new Map(); 44 | 45 | context.subscriptions.push(vscode.commands.registerCommand('extension.toggleRegexPreview', toggleRegexPreview)); 46 | 47 | languages.forEach(language => { 48 | context.subscriptions.push(vscode.languages.registerCodeLensProvider(language, { provideCodeLenses })); 49 | }); 50 | 51 | context.subscriptions.push(vscode.window.onDidChangeActiveTextEditor(() => updateDecorators(findRegexEditor()))); 52 | 53 | const interval = setInterval(() => updateDecorators(), 5000); 54 | context.subscriptions.push({ dispose: () => clearInterval(interval) }); 55 | 56 | function provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken) { 57 | const config = vscode.workspace.getConfiguration('regex-previewer', document.uri); 58 | if (!config.enableCodeLens) return; 59 | 60 | const matches = findRegexes(document); 61 | return matches.map(match => new vscode.CodeLens(match.range, { 62 | title: 'Test Regex...', 63 | command: 'extension.toggleRegexPreview', 64 | arguments: [ match ] 65 | })); 66 | } 67 | 68 | let addGMEnabled = false; 69 | const toggleGM = vscode.window.createStatusBarItem(); 70 | toggleGM.command = 'regexpreview.toggleGM'; 71 | context.subscriptions.push(toggleGM); 72 | context.subscriptions.push(vscode.commands.registerCommand('regexpreview.toggleGM', () => { 73 | addGMEnabled = !addGMEnabled; 74 | updateToggleGM(); 75 | for (const decorator of decorators.values()) { 76 | decorator.update(); 77 | } 78 | })) 79 | function updateToggleGM() { 80 | toggleGM.text = addGMEnabled ? 'Adding /gm' : 'Not adding /gm'; 81 | toggleGM.tooltip = addGMEnabled ? 'Click to stop adding global and multiline (/gm) options to regexes for evaluation with example text.' : 'Click to add global and multiline (/gm) options to regexes for evaluation with example text.' 82 | } 83 | updateToggleGM(); 84 | function addGM(regex: RegExp) { 85 | if (!addGMEnabled || (regex.global && regex.multiline)) { 86 | return regex; 87 | } 88 | 89 | let flags = regex.flags; 90 | if (!regex.global) { 91 | flags += 'g'; 92 | } 93 | if (!regex.multiline) { 94 | flags += 'm'; 95 | } 96 | return new RegExp(regex.source, flags); 97 | } 98 | 99 | let enabled = false; 100 | function toggleRegexPreview(initialRegexMatch?: RegexMatch) { 101 | enabled = !enabled || !!initialRegexMatch && !!initialRegexMatch.regex; 102 | toggleGM[enabled ? 'show' : 'hide'](); 103 | if (enabled) { 104 | const visibleEditors = getVisibleTextEditors(); 105 | if (visibleEditors.length === 1) { 106 | return openLoremIpsum(visibleEditors[0].viewColumn! + 1, initialRegexMatch); 107 | } else { 108 | updateDecorators(findRegexEditor(), initialRegexMatch); 109 | } 110 | } else { 111 | decorators.forEach(decorator => decorator.dispose()); 112 | } 113 | } 114 | 115 | function openLoremIpsum(column: number, initialRegexMatch?: RegexMatch) { 116 | return vscode.workspace.openTextDocument({ language: 'text', content: matchesFileContent }) 117 | .then(document => { 118 | return vscode.window.showTextDocument(document, column, true); 119 | }).then(editor => { 120 | updateDecorators(findRegexEditor(), initialRegexMatch); 121 | }).then(undefined, reason => { 122 | vscode.window.showErrorMessage(reason); 123 | }); 124 | } 125 | 126 | function updateDecorators(regexEditor?: vscode.TextEditor, initialRegexMatch?: RegexMatch) { 127 | if (!enabled) { 128 | return; 129 | } 130 | 131 | // TODO: figure out why originEditor.document is sometimes a different object 132 | if (regexEditor && initialRegexMatch && initialRegexMatch.document && initialRegexMatch.document.uri.toString() === regexEditor.document.uri.toString()) { 133 | initialRegexMatch.document = regexEditor.document; 134 | } 135 | 136 | const remove = new Map(decorators); 137 | getVisibleTextEditors().forEach(editor => { 138 | remove.delete(editor); 139 | applyDecorator(editor, regexEditor, initialRegexMatch); 140 | }); 141 | remove.forEach(decorator => decorator.dispose()); 142 | } 143 | 144 | function getVisibleTextEditors() { 145 | return vscode.window.visibleTextEditors.filter(editor => typeof editor.viewColumn === 'number'); 146 | } 147 | 148 | function applyDecorator(matchEditor: vscode.TextEditor, initialRegexEditor?: vscode.TextEditor, initialRegexMatch?: RegexMatch) { 149 | let decorator = decorators.get(matchEditor); 150 | const newDecorator = !decorator; 151 | if (newDecorator) { 152 | decorator = new RegexMatchDecorator(matchEditor); 153 | context.subscriptions.push(decorator); 154 | decorators.set(matchEditor, decorator); 155 | } 156 | if (newDecorator || initialRegexEditor || initialRegexMatch) { 157 | decorator!.apply(initialRegexEditor, initialRegexMatch); 158 | } 159 | } 160 | 161 | function discardDecorator(matchEditor: vscode.TextEditor) { 162 | decorators.delete(matchEditor); 163 | } 164 | 165 | interface RegexMatch { 166 | 167 | document: vscode.TextDocument; 168 | 169 | regex: RegExp; 170 | 171 | range: vscode.Range; 172 | 173 | } 174 | 175 | interface Match { 176 | 177 | range: vscode.Range; 178 | } 179 | 180 | class RegexMatchDecorator { 181 | 182 | private stableRegexEditor?: vscode.TextEditor; 183 | private stableRegexMatch?: RegexMatch; 184 | private disposables: vscode.Disposable[] = []; 185 | 186 | constructor(private matchEditor: vscode.TextEditor) { 187 | 188 | this.disposables.push(vscode.workspace.onDidCloseTextDocument(e => { 189 | if (this.stableRegexEditor && e === this.stableRegexEditor.document) { 190 | this.stableRegexEditor = undefined; 191 | this.stableRegexMatch = undefined; 192 | matchEditor.setDecorations(matchHighlight, []); 193 | } else if (e === matchEditor.document) { 194 | this.dispose(); 195 | } 196 | })); 197 | 198 | this.disposables.push(vscode.workspace.onDidChangeTextDocument(e => { 199 | if ((this.stableRegexEditor && e.document === this.stableRegexEditor.document) || e.document === matchEditor.document) { 200 | this.update(); 201 | } 202 | })); 203 | 204 | this.disposables.push(vscode.window.onDidChangeTextEditorSelection(e => { 205 | if (this.stableRegexEditor && e.textEditor === this.stableRegexEditor) { 206 | this.stableRegexMatch = undefined; 207 | this.update(); 208 | } 209 | })); 210 | 211 | this.disposables.push(vscode.window.onDidChangeActiveTextEditor(e => { 212 | this.update(); 213 | })); 214 | 215 | this.disposables.push({ dispose: () => { 216 | matchEditor.setDecorations(matchHighlight, []); 217 | matchEditor.setDecorations(regexHighlight, []); 218 | }}); 219 | } 220 | 221 | public apply(stableRegexEditor?: vscode.TextEditor, stableRegexMatch?: RegexMatch) { 222 | this.stableRegexEditor = stableRegexEditor; 223 | this.stableRegexMatch = stableRegexMatch; 224 | this.update(); 225 | } 226 | 227 | public dispose() { 228 | discardDecorator(this.matchEditor); 229 | this.disposables.forEach(disposable => { 230 | disposable.dispose(); 231 | }); 232 | } 233 | 234 | public update() { 235 | const regexEditor = this.stableRegexEditor = findRegexEditor() || this.stableRegexEditor; 236 | let regex = regexEditor && findRegexAtCaret(regexEditor); 237 | if (this.stableRegexMatch) { 238 | if (regex || !regexEditor || regexEditor.document !== this.stableRegexMatch.document) { 239 | this.stableRegexMatch = undefined; 240 | } else { 241 | regex = this.stableRegexMatch; 242 | } 243 | } 244 | const matches = regex && regexEditor !== this.matchEditor ? findMatches(regex, this.matchEditor.document) : []; 245 | this.matchEditor.setDecorations(matchHighlight, matches.map(match => match.range)); 246 | 247 | if (regexEditor) { 248 | regexEditor.setDecorations(regexHighlight, (this.stableRegexMatch || regexEditor !== vscode.window.activeTextEditor) && regex ? [ regex.range ] : []); 249 | } 250 | } 251 | } 252 | 253 | function findRegexEditor() { 254 | const activeEditor = vscode.window.activeTextEditor; 255 | if (!activeEditor || languages.indexOf(activeEditor.document.languageId) === -1) { 256 | return undefined; 257 | } 258 | return activeEditor; 259 | } 260 | 261 | function findRegexAtCaret(editor: vscode.TextEditor): RegexMatch | undefined { 262 | const anchor = editor.selection.anchor; 263 | const line = editor.document.lineAt(anchor); 264 | const text = line.text.substr(0, 1000); 265 | 266 | let match: RegExpExecArray | null; 267 | let regex = getRegexRegex(editor.document.languageId); 268 | regex.lastIndex = 0; 269 | while ((match = regex.exec(text)) && (match.index + match[1].length + match[2].length < anchor.character)); 270 | if (match && match.index + match[1].length <= anchor.character) { 271 | return createRegexMatch(editor.document, anchor.line, match); 272 | } 273 | } 274 | 275 | function findRegexes(document: vscode.TextDocument) { 276 | const matches: RegexMatch[] = []; 277 | for (let i = 0; i < document.lineCount; i++) { 278 | const line = document.lineAt(i); 279 | let match: RegExpExecArray | null; 280 | let regex = getRegexRegex(document.languageId); 281 | regex.lastIndex = 0; 282 | const text = line.text.substr(0, 1000); 283 | while ((match = regex.exec(text))) { 284 | const result = createRegexMatch(document, i, match); 285 | if (result) { 286 | matches.push(result); 287 | } 288 | } 289 | } 290 | return matches; 291 | } 292 | 293 | function getRegexRegex(languageId: String) { 294 | if (languageId == 'haxe') { 295 | return haxeRegexRegex; 296 | } else if (languageId == 'php') { 297 | return phpRegexRegex; 298 | } 299 | return regexRegex; 300 | } 301 | 302 | function createRegexMatch(document: vscode.TextDocument, line: number, match: RegExpExecArray) { 303 | const regex = createRegex(match[3], match[4]); 304 | if (regex) { 305 | return { 306 | document: document, 307 | regex: regex, 308 | range: new vscode.Range(line, match.index + match[1].length, line, match.index + match[1].length + match[2].length) 309 | }; 310 | } 311 | } 312 | 313 | function createRegex(pattern: string, flags: string) { 314 | try { 315 | return new RegExp(pattern, flags); 316 | } catch (e) { 317 | // discard 318 | } 319 | } 320 | 321 | function findMatches(regexMatch: RegexMatch, document: vscode.TextDocument) { 322 | const text = document.getText(); 323 | const matches: Match[] = []; 324 | const regex = addGM(regexMatch.regex); 325 | let match: RegExpExecArray | null; 326 | while ((regex.global || !matches.length) && (match = regex.exec(text))) { 327 | matches.push({ 328 | range: new vscode.Range(document.positionAt(match.index), document.positionAt(match.index + match[0].length)) 329 | }); 330 | // Handle empty matches (fixes #4) 331 | if (regex.lastIndex === match.index) { 332 | regex.lastIndex++; 333 | } 334 | } 335 | return matches; 336 | } 337 | } 338 | --------------------------------------------------------------------------------