├── .python-version ├── .gitattributes ├── README.md ├── LICENSE ├── plugin.py └── Default.sublime-keymap /.python-version: -------------------------------------------------------------------------------- 1 | 3.8 -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | images/ export-ignore 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | ![example](./images/output1.gif) 4 | 5 | This plugin converts a string to template strings in following cases. 6 | - When `${` is typed: 7 | ```jsx 8 | // | - is the cursor 9 | let x = 'Hello, ${|' 10 | let x = `Hello, ${|}` 11 | 12 | // in JSX 13 | const p =

14 | const p =

15 | ``` 16 | 17 | Press `undo`, to undo the conversion if you do not want template strings. 18 | ```jsx 19 | let x = 'Hello, ${|' 20 | let x = `Hello, ${|}` // press undo 21 | let x = "Hello, ${|}" 22 | ``` 23 | 24 | > This plugin is inspired by [Template String Converter](https://marketplace.visualstudio.com/items?itemName=meganrogge.template-string-converter) by Megan Rogge. 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Предраг Николић 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /plugin.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | import sublime_plugin 3 | 4 | 5 | class ConvertToTemplateString(sublime_plugin.TextCommand): 6 | """ This command is guaranteed to executed when { is pressed """ 7 | def run(self, edit): 8 | sel = self.view.sel() 9 | if not sel: 10 | return 11 | for r in sel: 12 | point = r.b 13 | if not in_supported_file(self.view, point): 14 | return None 15 | regular_string_region = get_regular_string_region(self.view, point) 16 | if not regular_string_region: 17 | continue 18 | scan_region = self.view.substr(regular_string_region) 19 | # the user could typed $ or { 20 | if "${" not in scan_region: 21 | continue 22 | first_quote = regular_string_region.begin() 23 | last_quote = regular_string_region.end() - 1 24 | are_quotes = is_regular_quote(self.view.substr(first_quote)) and is_regular_quote(self.view.substr(last_quote)) 25 | if not are_quotes: 26 | continue # sanity check, just to 100% make sure we are replacing quotes 27 | if is_jsx_attribute(self.view, point) and not is_jsx_attribute_wrapped_with_curly_brackets(self.view, point): 28 | # insert surrounding curly brackets 29 | self.view.replace( 30 | edit, sublime.Region(last_quote, last_quote + 1), '`}') 31 | self.view.replace( 32 | edit, sublime.Region(first_quote, first_quote + 1), '{`') 33 | continue 34 | self.view.replace( 35 | edit, sublime.Region(last_quote, last_quote + 1), '`') 36 | self.view.replace( 37 | edit, sublime.Region(first_quote, first_quote + 1), '`') 38 | 39 | 40 | def is_jsx_attribute(view: sublime.View, point: int) -> bool: 41 | return view.match_selector(point, "meta.jsx meta.tag.attributes") 42 | 43 | 44 | def is_jsx_attribute_wrapped_with_curly_brackets(view: sublime.View, point: int) -> bool: 45 | return view.match_selector(point, "meta.jsx meta.tag.attributes meta.interpolation meta.string string.quoted") 46 | 47 | 48 | def is_regular_quote(char: str) -> bool: 49 | return char in ["'", '"'] 50 | 51 | 52 | def in_supported_file(view: sublime.View, point: int) -> bool: 53 | return view.match_selector(point, "source.js | source.jsx | source.ts | source.tsx | text.html.ngx | text.html.svelte | text.html.vue") 54 | 55 | 56 | def get_regular_string_region(view: sublime.View, point: int): 57 | return view.expand_to_scope(point, "string.quoted.single | string.quoted.double") 58 | -------------------------------------------------------------------------------- /Default.sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | {"keys": ["$"], "command": "chain", 3 | "args": { 4 | "commands": [ 5 | {"command": "insert_snippet", "args": {"contents": "\\$"}}, 6 | {"command": "convert_to_template_string"}, 7 | ] 8 | }, 9 | "context": [ 10 | { "key": "setting.auto_match_enabled" }, 11 | { "key": "selector", "operand": "source.js | source.jsx | source.ts | source.tsx | text.html.ngx | text.html.svelte | text.html.vue" }, 12 | { "key": "selection_empty", "match_all": true }, 13 | { "key": "following_text", "operator": "regex_contains", "operand": "^\\{", "match_all": true } 14 | ]}, 15 | {"keys": ["{"], "command": "chain", 16 | "args": { 17 | "commands": [ 18 | {"command": "insert_snippet", "args": {"contents": "{$0}"}}, 19 | {"command": "convert_to_template_string"}, 20 | ] 21 | }, 22 | "context": [ 23 | { "key": "setting.auto_match_enabled" }, 24 | { "key": "selector", "operand": "source.js | source.jsx | source.ts | source.tsx | text.html.ngx | text.html.svelte | text.html.vue" }, 25 | { "key": "selection_empty", "match_all": true }, 26 | { "key": "preceding_text", "operator": "regex_contains", "operand": "\\$", "match_all": true }, 27 | ]}, 28 | // move cursor after '}' 29 | // if '}' is in front of cursor 30 | {"keys": ["}"], "command": "chain", 31 | "args": { 32 | "commands": [ 33 | {"command": "move", "args": {"by": "characters", "forward": true}}, 34 | {"command": "convert_to_template_string"}, 35 | ] 36 | }, 37 | "context": [ 38 | { "key": "setting.auto_match_enabled" }, 39 | { "key": "selector", "operand": "source.js | source.jsx | source.ts | source.tsx | text.html.ngx | text.html.svelte | text.html.vue" }, 40 | { "key": "selection_empty", "match_all": true }, 41 | { "key": "preceding_text", "operator": "regex_contains", "operand": "\\$\\{.?+", "match_all": true }, 42 | { "key": "following_text", "operator": "regex_contains", "operand": "^\\}", "match_all": true } 43 | ]}, 44 | // insert '}' 45 | // if '}' is not in front of cursor 46 | {"keys": ["}"], "command": "chain", 47 | "args": { 48 | "commands": [ 49 | {"command": "insert_snippet", "args": {"contents": "}"}}, 50 | {"command": "convert_to_template_string"}, 51 | ] 52 | }, 53 | "context": [ 54 | { "key": "setting.auto_match_enabled" }, 55 | { "key": "selector", "operand": "source.js | source.jsx | source.ts | source.tsx | text.html.ngx | text.html.svelte | text.html.vue" }, 56 | { "key": "selection_empty", "match_all": true }, 57 | { "key": "preceding_text", "operator": "regex_contains", "operand": "\\$\\{.?+", "match_all": true }, 58 | { "key": "following_text", "operator": "not_regex_contains", "operand": "^\\}", "match_all": true } 59 | ]}, 60 | ] 61 | --------------------------------------------------------------------------------