├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.yml │ └── bug_report.yml ├── dependabot.yml ├── FUNDING.yml ├── workflows │ ├── nvim-type-check.yml │ ├── stylua.yml │ ├── markdownlint.yml │ ├── stale-bot.yml │ ├── pr-title.yml │ └── panvimdoc.yml └── pull_request_template.md ├── .gitignore ├── .stylua.toml ├── .emmyrc.json ├── .editorconfig ├── .luarc.jsonc ├── .markdownlint.yaml ├── LICENSE ├── plugin └── puppeteer-autocmds.lua ├── README.md ├── lua └── puppeteer.lua └── doc └── nvim-puppeteer.txt /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # help-tags auto-generated by lazy.nvim 2 | doc/tags 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | commit-message: 8 | prefix: "chore(dependabot): " 9 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/github/administering-a-repository/managing-repository-settings/displaying-a-sponsor-button-in-your-repository 2 | 3 | custom: https://www.paypal.me/ChrisGrieser 4 | ko_fi: pseudometa 5 | -------------------------------------------------------------------------------- /.stylua.toml: -------------------------------------------------------------------------------- 1 | # https://github.com/JohnnyMorganz/StyLua#options 2 | column_width = 105 3 | indent_type = "Tabs" 4 | indent_width = 3 5 | quote_style = "AutoPreferDouble" 6 | call_parentheses = "NoSingleTable" 7 | collapse_simple_statement = "Always" 8 | 9 | [sort_requires] 10 | enabled = true 11 | -------------------------------------------------------------------------------- /.github/workflows/nvim-type-check.yml: -------------------------------------------------------------------------------- 1 | name: nvim type check 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: ["**.lua"] 7 | pull_request: 8 | paths: ["**.lua"] 9 | 10 | jobs: 11 | build: 12 | name: nvim type check 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v6 16 | - uses: stevearc/nvim-typecheck-action@v2 17 | -------------------------------------------------------------------------------- /.emmyrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "runtime": { 3 | "version": "LuaJIT", 4 | "requirePattern": ["lua/?.lua", "lua/?/init.lua"] 5 | }, 6 | "workspace": { 7 | "library": [ 8 | "$VIMRUNTIME", 9 | "$HOME/.local/share/nvim/lazy/luvit-meta/library/uv.lua" 10 | ] 11 | }, 12 | "$schema": "https://raw.githubusercontent.com/EmmyLuaLs/emmylua-analyzer-rust/refs/heads/main/crates/emmylua_code_analysis/resources/schema.json" 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/stylua.yml: -------------------------------------------------------------------------------- 1 | name: Stylua check 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: ["**.lua"] 7 | pull_request: 8 | paths: ["**.lua"] 9 | 10 | jobs: 11 | stylua: 12 | name: Stylua 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v6 16 | - uses: JohnnyMorganz/stylua-action@v4 17 | with: 18 | token: ${{ secrets.GITHUB_TOKEN }} 19 | version: latest 20 | args: --check . 21 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | max_line_length = 100 5 | end_of_line = lf 6 | charset = utf-8 7 | insert_final_newline = true 8 | indent_style = tab 9 | indent_size = 3 10 | tab_width = 3 11 | trim_trailing_whitespace = true 12 | 13 | [*.{yml,yaml,scm,cff}] 14 | indent_style = space 15 | indent_size = 2 16 | tab_width = 2 17 | 18 | [*.py] 19 | indent_style = space 20 | indent_size = 4 21 | tab_width = 4 22 | 23 | [*.md] 24 | indent_size = 4 25 | tab_width = 4 26 | trim_trailing_whitespace = false 27 | -------------------------------------------------------------------------------- /.luarc.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "runtime.version": "LuaJIT", 3 | 4 | "workspace.library": [ 5 | "$VIMRUNTIME/lua", // nvim-lua runtime 6 | "${3rd}/luv/library" // vim.uv 7 | ], 8 | 9 | "diagnostics": { 10 | "unusedLocalExclude": ["_*"], // allow `_varname` for unused variables 11 | "groupFileStatus": { 12 | "luadoc": "Any", // require stricter annotations 13 | "conventions": "Any" // disallow global variables 14 | } 15 | }, 16 | 17 | "$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json" 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/markdownlint.yml: -------------------------------------------------------------------------------- 1 | name: Markdownlint check 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: 7 | - "**.md" 8 | - ".github/workflows/markdownlint.yml" 9 | - ".markdownlint.*" # markdownlint config files 10 | pull_request: 11 | paths: 12 | - "**.md" 13 | 14 | jobs: 15 | markdownlint: 16 | name: Markdownlint 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v6 20 | - uses: DavidAnson/markdownlint-cli2-action@v22 21 | with: 22 | globs: "**/*.md" 23 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Problem statement 2 | 3 | 4 | ## Proposed solution 5 | 6 | 7 | ## AI usage disclosure 8 | 11 | 12 | ## Checklist 13 | - [ ] Variable names follow `camelCase` convention. 14 | - [ ] All AI-generated code has been reviewed by a human. 15 | - [ ] The `README.md` has been updated for any new or modified functionality 16 | (the `.txt` file is auto-generated and does not need to be modified). 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest an idea 3 | title: "Feature Request: " 4 | labels: ["enhancement"] 5 | body: 6 | - type: textarea 7 | id: feature-requested 8 | attributes: 9 | label: Feature Requested 10 | description: A clear and concise description of the feature. 11 | validations: 12 | required: true 13 | - type: textarea 14 | id: screenshot 15 | attributes: 16 | label: Relevant Screenshot 17 | description: If applicable, add screenshots or a screen recording to help explain the request. 18 | - type: checkboxes 19 | id: checklist 20 | attributes: 21 | label: Checklist 22 | options: 23 | - label: The feature would be useful to more users than just me. 24 | required: true 25 | -------------------------------------------------------------------------------- /.github/workflows/stale-bot.yml: -------------------------------------------------------------------------------- 1 | name: Stale bot 2 | on: 3 | schedule: 4 | - cron: "18 04 * * 3" 5 | 6 | permissions: 7 | issues: write 8 | pull-requests: write 9 | 10 | jobs: 11 | stale: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Close stale issues 15 | uses: actions/stale@v10 16 | with: 17 | repo-token: ${{ secrets.GITHUB_TOKEN }} 18 | 19 | # DOCS https://github.com/actions/stale#all-options 20 | days-before-stale: 180 21 | days-before-close: 7 22 | stale-issue-label: "Stale" 23 | stale-issue-message: | 24 | This issue has been automatically marked as stale. 25 | **If this issue is still affecting you, please leave any comment**, for example "bump", and it will be kept open. 26 | close-issue-message: | 27 | This issue has been closed due to inactivity, and will not be monitored. 28 | -------------------------------------------------------------------------------- /.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | # DOCS https://github.com/markdownlint/markdownlint/blob/main/docs/RULES.md 2 | #------------------------------------------------------------------------------- 3 | 4 | default: warning 5 | 6 | #-MODIFIED SETTINGS------------------------------------------------------------- 7 | blanks-around-headings: { lines_below: 0 } # rule of proximity 8 | ul-style: { style: sublist } 9 | ol-prefix: { style: ordered } 10 | line-length: { tables: false, code_block_line_length: 90 } 11 | no-inline-html: { allowed_elements: [img, details, summary, kbd, a, table, tr, th, td] } 12 | 13 | #-DISABLED---------------------------------------------------------------------- 14 | ul-indent: false # not compatible with using tabs 15 | no-hard-tabs: false # taken care of by .editorconfig 16 | blanks-around-lists: false # space waster 17 | first-line-heading: false # ignore-comments for linters can be in the first line 18 | no-emphasis-as-heading: false # for small sections that shouldn't be linked in the ToC 19 | -------------------------------------------------------------------------------- /.github/workflows/pr-title.yml: -------------------------------------------------------------------------------- 1 | name: PR title 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | - reopened 10 | - ready_for_review 11 | 12 | permissions: 13 | pull-requests: read 14 | 15 | jobs: 16 | semantic-pull-request: 17 | name: Check PR title 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: amannn/action-semantic-pull-request@v6 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | with: 24 | requireScope: false 25 | subjectPattern: ^(?![A-Z]).+$ # disallow title starting with capital 26 | types: | # add `improv` to the list of allowed types 27 | improv 28 | fix 29 | feat 30 | refactor 31 | build 32 | ci 33 | style 34 | test 35 | chore 36 | perf 37 | docs 38 | break 39 | revert 40 | -------------------------------------------------------------------------------- /.github/workflows/panvimdoc.yml: -------------------------------------------------------------------------------- 1 | name: panvimdoc 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: 7 | - README.md 8 | - .github/workflows/panvimdoc.yml 9 | workflow_dispatch: {} # allows manual execution 10 | 11 | permissions: 12 | contents: write 13 | 14 | #─────────────────────────────────────────────────────────────────────────────── 15 | 16 | jobs: 17 | docs: 18 | runs-on: ubuntu-latest 19 | name: README.md to vimdoc 20 | steps: 21 | - uses: actions/checkout@v6 22 | - run: git pull # fix failure when multiple commits are pushed in succession 23 | - run: mkdir -p doc 24 | 25 | - name: panvimdoc 26 | uses: kdheepak/panvimdoc@main 27 | with: 28 | vimdoc: ${{ github.event.repository.name }} 29 | version: "Neovim" 30 | demojify: true 31 | treesitter: true 32 | 33 | - run: git pull 34 | - name: push changes 35 | uses: stefanzweifel/git-auto-commit-action@v7 36 | with: 37 | commit_message: "chore: auto-generate vimdocs" 38 | branch: ${{ github.head_ref }} 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Christopher Grieser 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 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | title: "Bug: " 4 | labels: ["bug"] 5 | body: 6 | - type: checkboxes 7 | id: checklist 8 | attributes: 9 | label: Make sure you have done the following 10 | options: 11 | - label: I have updated to the latest version of the plugin. 12 | required: true 13 | - label: I have read the README. 14 | required: true 15 | - label: > 16 | All Treesitter parsers for the filetypes used by nvim-puppeteer are installed, and I updated 17 | all my Treesitter parsers via `:TSUpdate`. 18 | required: true 19 | - type: textarea 20 | id: bug-description 21 | attributes: 22 | label: Bug Description 23 | description: A clear and concise description of the bug. 24 | validations: { required: true } 25 | - type: textarea 26 | id: screenshot 27 | attributes: 28 | label: Relevant Screenshot 29 | description: If applicable, add screenshots or a screen recording to help explain your problem. 30 | - type: textarea 31 | id: reproduction-steps 32 | attributes: 33 | label: To Reproduce 34 | description: Please include example code where the plugin is not working. 35 | placeholder: | 36 | For example: 37 | 1. In the filetype… 38 | 2. Given a string like this … with the cursor positioned at… 39 | validations: { required: true } 40 | - type: textarea 41 | id: version-info 42 | attributes: 43 | label: neovim version 44 | render: Text 45 | validations: { required: true } 46 | -------------------------------------------------------------------------------- /plugin/puppeteer-autocmds.lua: -------------------------------------------------------------------------------- 1 | local supportedFiletypes = { 2 | python = "pythonFStr", 3 | lua = "luaFormatStr", 4 | javascript = "templateStr", 5 | typescript = "templateStr", 6 | javascriptreact = "templateStr", 7 | typescriptreact = "templateStr", 8 | vue = "templateStr", 9 | astro = "templateStr", 10 | svelte = "templateStr", 11 | } 12 | 13 | -- disable puppeteer for certain filetypes based on user config 14 | for _, ft in pairs(vim.g.puppeteer_disabled_filetypes or {}) do 15 | supportedFiletypes[ft] = nil 16 | end 17 | 18 | -------------------------------------------------------------------------------- 19 | local activeFiletypes = vim.tbl_keys(supportedFiletypes) 20 | vim.api.nvim_create_autocmd("FileType", { 21 | group = vim.api.nvim_create_augroup("nvim-puppeteer-autocmd-setup", { clear = true }), 22 | desc = "nvim-puppeteer trigger to set up buffer-specific autocmds", 23 | pattern = activeFiletypes, 24 | callback = function(ctx) 25 | local ft = ctx.match 26 | local stringTransformFunc = require("puppeteer")[supportedFiletypes[ft]] 27 | local groupForBuffer = ("nvim-puppeteer-trigger_%d"):format(ctx.buf) 28 | 29 | vim.api.nvim_create_autocmd({ "InsertLeave", "TextChanged" }, { 30 | buffer = 0, 31 | group = vim.api.nvim_create_augroup(groupForBuffer, { clear = true }), 32 | desc = "nvim-puppeteer trigger for supported filetypes", 33 | callback = function(ctx2) 34 | local bufnr = ctx2.buf 35 | 36 | -- if buffer changed ft, disable this autocmd see #19 37 | -- (returning `true` deletes an autocmd) 38 | if not vim.tbl_contains(activeFiletypes, vim.bo[bufnr].ft) then return true end 39 | 40 | if 41 | vim.b[bufnr].puppeteer_enabled == false 42 | or vim.bo[bufnr].buftype ~= "" 43 | or not (vim.api.nvim_buf_is_valid(bufnr)) 44 | then 45 | return 46 | end 47 | -- deferred to prevent race conditions with other autocmds 48 | vim.defer_fn(stringTransformFunc, 1) 49 | end, 50 | }) 51 | end, 52 | }) 53 | 54 | -------------------------------------------------------------------------------- 55 | -- USER COMMANDS 56 | 57 | ---@param mode "Enabled"|"Disabled" 58 | local function notify(mode) vim.notify(mode .. " for current buffer.", nil, { title = "nvim-puppeteer" }) end 59 | 60 | vim.api.nvim_create_user_command("PuppeteerDisable", function() 61 | vim.b.puppeteer_enabled = false 62 | notify("Disabled") 63 | end, { desc = "Disable puppeteer for the current buffer" }) 64 | 65 | vim.api.nvim_create_user_command("PuppeteerEnable", function() 66 | vim.b.puppeteer_enabled = true 67 | notify("Enabled") 68 | end, { desc = "Enable puppeteer for the current buffer" }) 69 | 70 | vim.api.nvim_create_user_command("PuppeteerToggle", function() 71 | if vim.b.puppeteer_enabled == true or vim.b.puppeteer_enabled == nil then 72 | vim.b.puppeteer_enabled = false 73 | notify("Disabled") 74 | else 75 | vim.b.puppeteer_enabled = true 76 | notify("Enabled") 77 | end 78 | end, { desc = "Toggle puppeteer for the current buffer" }) 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # nvim-puppeteer 🎎 3 | 4 | 5 | Shield 6 | 7 | Master of strings. Automatically convert strings to f-strings or template 8 | strings and back. 9 | 10 | 11 | 12 | - [Features](#features) 13 | - [Requirements](#requirements) 14 | - [Installation](#installation) 15 | - [Configuration](#configuration) 16 | - [User commands](#user-commands) 17 | - [Special case: formatted strings in Lua](#special-case-formatted-strings-in-lua) 18 | - [Credits](#credits) 19 | 20 | 21 | 22 | ## Features 23 | - When typing `{}` in a **Python string** automatically converts it to an f-string. 24 | - Adding `${}` or a line break in a **JavaScript string** automatically converts 25 | it to a template string. (Also works in related languages like JS-React or 26 | Typescript.) 27 | - Typing `%s` in a **non-pattern Lua string** automatically converts it to a 28 | formatted string. (Opt-in, as this has [some 29 | caveats](#special-case-formatted-strings-in-lua).) 30 | - *Removing* the `{}`, `${}`, or `%s` converts it back to a regular string. 31 | - Also works with multi-line strings and undos. 32 | - Zero configuration. Just install and you are ready to go. 33 | 34 | ## Requirements 35 | - nvim 0.9 or higher. 36 | - The respective Treesitter parsers: `:TSInstall python javascript typescript`. 37 | (Installing them requires 38 | [nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter).) 39 | 40 | ## Installation 41 | 42 | ```lua 43 | -- lazy.nvim 44 | { 45 | "chrisgrieser/nvim-puppeteer", 46 | lazy = false, -- plugin lazy-loads itself. Can also load on filetypes. 47 | }, 48 | 49 | -- packer 50 | use { "chrisgrieser/nvim-puppeteer" } 51 | ``` 52 | 53 | There is no `.setup()` call. The plugin already automatically lazy-loads itself 54 | (and is lightweight to begin with). 55 | 56 | You can disable `nvim-puppeteer` only for specific filetypes via: 57 | 58 | ## Configuration 59 | Shown are the default values. 60 | 61 | ```lua 62 | -- list of filestypes (please see the README before enabling this plugin for lua) 63 | vim.g.puppeteer_disable_filetypes = { "lua" } 64 | 65 | -- quotation mark to use when converting back to normal string (" or ') 66 | vim.g.puppeteer_js_quotation_mark = '"' 67 | ``` 68 | 69 | > [!NOTE] 70 | > When using `lazy.nvim`, `vim.g.…` variables must be set in `init`, not in 71 | > `config`. 72 | 73 | ## User commands 74 | The plugin is enabled by default and lazy-loaded upon opening a relevant file type. 75 | In case you wish to turn of puppeteer for the current buffer, the following user 76 | commands are provided: 77 | 78 | - `PuppeteerToggle`: Toggle puppeteer for the current buffer. 79 | - `PuppeteerDisable`: Disable puppeteer for the current buffer. 80 | - `PuppeteerEnable`: Enable puppeteer for the current buffer. 81 | 82 | ## Special case: formatted strings in Lua 83 | Through 84 | [string.format](https://www.lua.org/manual/5.4/manual.html#pdf-string.format), 85 | there are also formatted strings in Lua. However, auto-conversions are far more 86 | difficult in lua `%s` is used as a placeholder for `string.format` and [as class 87 | in lua patterns](https://www.lua.org/manual/5.4/manual.html#6.4.1) at the same 88 | time. While it is possible to identify in some cases whether a lua string is 89 | used as pattern, there are certain cases where that is not possible: 90 | 91 | ```lua 92 | -- desired: conversion to format string when typing the placeholder "%s" 93 | local str = "foobar %s baz" -- before 94 | local str = ("foobar %s baz"):format() -- after 95 | 96 | -- problem case that can be dealt with: "%s" used as class in lua pattern 97 | local found = str:find("foobar %s") 98 | 99 | -- problem case that cannot be dealt with: "%s" in string, which 100 | -- is only later used as pattern 101 | local pattern = "foobar %s baz" 102 | -- some code… 103 | str:find(pattern) 104 | ``` 105 | 106 | Since the auto-conversion of lua strings can result in undesired false 107 | conversions, the feature is opt-in only. This way, you can decide for yourself 108 | whether the occasional false positive is worth it for you or not. 109 | 110 | ```lua 111 | -- Enable auto-conversion of lua strings by removing lua from the disabled filetypes 112 | vim.g.puppeteer_disable_filetypes = {} 113 | ``` 114 | 115 | > [!TIP] 116 | > You can also use `PuppeteerToggle` to temporarily disable the plugin for the 117 | > current buffer, if a specific lua string is giving you trouble. 118 | 119 | 120 | ## Credits 121 | In my day job, I am a sociologist studying the social mechanisms underlying the 122 | digital economy. For my PhD project, I investigate the governance of the app 123 | economy and how software ecosystems manage the tension between innovation and 124 | compatibility. If you are interested in this subject, feel free to get in touch. 125 | 126 | - [Website](https://chris-grieser.de/) 127 | - [Mastodon](https://pkm.social/@pseudometa) 128 | - [ResearchGate](https://www.researchgate.net/profile/Christopher-Grieser) 129 | - [LinkedIn](https://www.linkedin.com/in/christopher-grieser-ba693b17a/) 130 | 131 | Buy Me a Coffee at ko-fi.com 134 | -------------------------------------------------------------------------------- /lua/puppeteer.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | -------------------------------------------------------------------------------- 3 | 4 | ---@param node TSNode 5 | ---@param replacementText string 6 | local function replaceNodeText(node, replacementText) 7 | local startRow, startCol, endRow, endCol = node:range() 8 | local lines = vim.split(replacementText, "\n") 9 | pcall(vim.cmd.undojoin) -- make undos ignore the next change, see #8 10 | vim.api.nvim_buf_set_text(0, startRow, startCol, endRow, endCol, lines) 11 | end 12 | 13 | ---get node at cursor and validate that the user has at least nvim 0.9 14 | ---@return nil|TSNode nil if no node or nvim version too old 15 | local function getNodeAtCursor() 16 | if vim.treesitter.get_node == nil then 17 | vim.notify("nvim-puppeteer requires at least nvim 0.9.", vim.log.levels.WARN) 18 | return 19 | end 20 | return vim.treesitter.get_node() 21 | end 22 | 23 | ---@param node TSNode 24 | ---@return string 25 | local function getNodeText(node) return vim.treesitter.get_node_text(node, 0) end 26 | 27 | -------------------------------------------------------------------------------- 28 | 29 | -- CONFIG 30 | -- safeguard to prevent converting invalid code 31 | local maxCharacters = 200 32 | 33 | -------------------------------------------------------------------------------- 34 | 35 | -- auto-convert string to template string and back 36 | function M.templateStr() 37 | local node = getNodeAtCursor() 38 | if not node then return end 39 | if node:type() == "string_fragment" or node:type() == "escape_sequence" then node = node:parent() end 40 | if not node then return end 41 | local text = getNodeText(node) 42 | 43 | if 44 | not (node:type() == "string" or node:type() == "template_string") 45 | or text == "" -- user might want to enter sth 46 | or #text > maxCharacters -- safeguard against converting invalid code 47 | then 48 | return 49 | end 50 | 51 | -- not checking via node-type, since treesitter sometimes does not update that in time 52 | local isTemplateStr = text:find("^`.*`$") 53 | local isTaggedTemplate = node:parent():type() == "call_expression" 54 | local isMultilineString = text:find("[\n\r]") 55 | local hasBraces = text:find("%${.-}") 56 | 57 | if not isTemplateStr and (hasBraces or isMultilineString) then 58 | text = "`" .. text:sub(2, -2) .. "`" 59 | replaceNodeText(node, text) 60 | elseif isTemplateStr and not (hasBraces or isMultilineString or isTaggedTemplate) then 61 | local quote = vim.g.puppeteer_js_quotation_mark == "'" and "'" or '"' 62 | text = quote .. text:sub(2, -2) .. quote 63 | replaceNodeText(node, text) 64 | end 65 | end 66 | 67 | -- auto-convert string to f-string and back 68 | function M.pythonFStr() 69 | local node = getNodeAtCursor() 70 | if not node then return end 71 | 72 | local strNode 73 | if node:type() == "string" then 74 | strNode = node 75 | elseif node:type():find("^string_") then 76 | strNode = node:parent() 77 | elseif node:type() == "escape_sequence" then 78 | strNode = node:parent():parent() 79 | else 80 | return 81 | end 82 | if not strNode then return end 83 | local text = getNodeText(strNode) 84 | 85 | -- GUARD 86 | if text == "" then return end -- don't convert empty strings, user might want to enter sth 87 | if #text > maxCharacters then return end -- safeguard on converting invalid code 88 | 89 | local isFString = text:find("^r?f") -- rf -> raw-formatted-string 90 | local hasBraces = text:find("{.-[^%d,%s].-}") -- nonRegex-braces, see #12 and #15 91 | 92 | if not isFString and hasBraces then 93 | text = "f" .. text 94 | replaceNodeText(strNode, text) 95 | elseif isFString and not hasBraces then 96 | text = text:sub(2) 97 | replaceNodeText(strNode, text) 98 | end 99 | end 100 | 101 | -------------------------------------------------------------------------------- 102 | 103 | function M.luaFormatStr() 104 | local node = getNodeAtCursor() 105 | if not node then return end 106 | local strNode 107 | if node:type() == "string" then 108 | strNode = node 109 | elseif node:type():find("string_content") then 110 | strNode = node:parent() 111 | elseif node:type() == "escape_sequence" then 112 | strNode = node:parent():parent() 113 | else 114 | return 115 | end 116 | if not strNode then return end 117 | local text = getNodeText(strNode) 118 | 119 | -- GUARD 120 | -- lua patterns (string.match, …) use `%s` as class patterns 121 | -- this works with string.match() as well as var:match() 122 | local stringMethod = strNode:parent() 123 | and strNode:parent():prev_sibling() 124 | and strNode:parent():prev_sibling():child(2) 125 | local methodText = stringMethod and getNodeText(stringMethod) or "" 126 | local isLuaPattern = vim.tbl_contains({ "match", "gmatch", "find", "gsub" }, methodText) 127 | local likelyLuaPattern = text:find("%%[waudglpfb]") or text:find("%%s[*+-]") 128 | if isLuaPattern or likelyLuaPattern then return end 129 | if text == "" then return end -- don't convert empty strings, user might want to enter sth 130 | if #text > maxCharacters then return end -- safeguard on converting invalid code 131 | 132 | -- REPLACE TEXT 133 | -- string format: https://www.lua.org/manual/5.4/manual.html#pdf-string.format 134 | -- patterns: https://www.lua.org/manual/5.4/manual.html#6.4.1 135 | local hasPlaceholder = (text:find("%%[sq]") and not text:find("%%s[*+-]")) or text:find("%%06[Xx]") 136 | local isFormatString = strNode:parent():type() == "parenthesized_expression" 137 | 138 | if hasPlaceholder and not isFormatString then 139 | replaceNodeText(strNode, "(" .. text .. "):format()") 140 | -- move cursor so user can insert there directly 141 | local row, col = strNode:end_() 142 | vim.api.nvim_win_set_cursor(0, { row + 1, col - 1 }) 143 | vim.cmd.startinsert() 144 | elseif isFormatString and not hasPlaceholder then 145 | local formatCall = strNode:parent():parent():parent() 146 | if not formatCall then return end 147 | local removedFormat = getNodeText(formatCall):gsub("%((.*)%):format%(.*%)", "%1") 148 | replaceNodeText(formatCall, removedFormat) 149 | end 150 | end 151 | 152 | -------------------------------------------------------------------------------- 153 | return M 154 | -------------------------------------------------------------------------------- /doc/nvim-puppeteer.txt: -------------------------------------------------------------------------------- 1 | *nvim-puppeteer.txt* For Neovim Last change: 2025 November 24 2 | 3 | ============================================================================== 4 | Table of Contents *nvim-puppeteer-table-of-contents* 5 | 6 | 1. nvim-puppeteer |nvim-puppeteer-nvim-puppeteer-| 7 | - Features |nvim-puppeteer-nvim-puppeteer--features| 8 | - Requirements |nvim-puppeteer-nvim-puppeteer--requirements| 9 | - Installation |nvim-puppeteer-nvim-puppeteer--installation| 10 | - Configuration |nvim-puppeteer-nvim-puppeteer--configuration| 11 | - User commands |nvim-puppeteer-nvim-puppeteer--user-commands| 12 | - Special case: formatted strings in Lua|nvim-puppeteer-nvim-puppeteer--special-case:-formatted-strings-in-lua| 13 | - Credits |nvim-puppeteer-nvim-puppeteer--credits| 14 | 15 | ============================================================================== 16 | 1. nvim-puppeteer *nvim-puppeteer-nvim-puppeteer-* 17 | 18 | 19 | 20 | Masterof strings. Automatically convert strings to f-strings or template 21 | strings and back. 22 | 23 | - |nvim-puppeteer-features| 24 | - |nvim-puppeteer-requirements| 25 | - |nvim-puppeteer-installation| 26 | - |nvim-puppeteer-configuration| 27 | - |nvim-puppeteer-user-commands| 28 | - |nvim-puppeteer-special-case:-formatted-strings-in-lua| 29 | - |nvim-puppeteer-credits| 30 | 31 | 32 | FEATURES *nvim-puppeteer-nvim-puppeteer--features* 33 | 34 | - When typing `{}` in a **Python string** automatically converts it to an f-string. 35 | - Adding `${}` or a line break in a **JavaScript string** automatically converts 36 | it to a template string. (Also works in related languages like JS-React or 37 | Typescript.) 38 | - Typing `%s` in a **non-pattern Lua string** automatically converts it to a 39 | formatted string. (Opt-in, as this has |nvim-puppeteer-some-caveats|.) 40 | - _Removing_ the `{}`, `${}`, or `%s` converts it back to a regular string. 41 | - Also works with multi-line strings and undos. 42 | - Zero configuration. Just install and you are ready to go. 43 | 44 | 45 | REQUIREMENTS *nvim-puppeteer-nvim-puppeteer--requirements* 46 | 47 | - nvim 0.9 or higher. 48 | - The respective Treesitter parsers: `:TSInstall python javascript typescript`. 49 | (Installing them requires 50 | nvim-treesitter .) 51 | 52 | 53 | INSTALLATION *nvim-puppeteer-nvim-puppeteer--installation* 54 | 55 | >lua 56 | -- lazy.nvim 57 | { 58 | "chrisgrieser/nvim-puppeteer", 59 | lazy = false, -- plugin lazy-loads itself. Can also load on filetypes. 60 | }, 61 | 62 | -- packer 63 | use { "chrisgrieser/nvim-puppeteer" } 64 | < 65 | 66 | There is no `.setup()` call. The plugin already automatically lazy-loads itself 67 | (and is lightweight to begin with). 68 | 69 | You can disable `nvim-puppeteer` only for specific filetypes via: 70 | 71 | 72 | CONFIGURATION *nvim-puppeteer-nvim-puppeteer--configuration* 73 | 74 | Shown are the default values. 75 | 76 | >lua 77 | -- list of filestypes (please see the README before enabling this plugin for lua) 78 | vim.g.puppeteer_disable_filetypes = { "lua" } 79 | 80 | -- quotation mark to use when converting back to normal string (" or ') 81 | vim.g.puppeteer_js_quotation_mark = '"' 82 | < 83 | 84 | 85 | [!NOTE] When using `lazy.nvim`, `vim.g.…` variables must be set in `init`, 86 | not in `config`. 87 | 88 | USER COMMANDS *nvim-puppeteer-nvim-puppeteer--user-commands* 89 | 90 | The plugin is enabled by default and lazy-loaded upon opening a relevant file 91 | type. In case you wish to turn of puppeteer for the current buffer, the 92 | following user commands are provided: 93 | 94 | - `PuppeteerToggle`: Toggle puppeteer for the current buffer. 95 | - `PuppeteerDisable`: Disable puppeteer for the current buffer. 96 | - `PuppeteerEnable`: Enable puppeteer for the current buffer. 97 | 98 | 99 | SPECIAL CASE: FORMATTED STRINGS IN LUA*nvim-puppeteer-nvim-puppeteer--special-case:-formatted-strings-in-lua* 100 | 101 | Through string.format 102 | , there are also 103 | formatted strings in Lua. However, auto-conversions are far more difficult in 104 | lua `%s` is used as a placeholder for `string.format` and as class in lua 105 | patterns at the same time. 106 | While it is possible to identify in some cases whether a lua string is used as 107 | pattern, there are certain cases where that is not possible: 108 | 109 | >lua 110 | -- desired: conversion to format string when typing the placeholder "%s" 111 | local str = "foobar %s baz" -- before 112 | local str = ("foobar %s baz"):format() -- after 113 | 114 | -- problem case that can be dealt with: "%s" used as class in lua pattern 115 | local found = str:find("foobar %s") 116 | 117 | -- problem case that cannot be dealt with: "%s" in string, which 118 | -- is only later used as pattern 119 | local pattern = "foobar %s baz" 120 | -- some code… 121 | str:find(pattern) 122 | < 123 | 124 | Since the auto-conversion of lua strings can result in undesired false 125 | conversions, the feature is opt-in only. This way, you can decide for yourself 126 | whether the occasional false positive is worth it for you or not. 127 | 128 | >lua 129 | -- Enable auto-conversion of lua strings by removing lua from the disabled filetypes 130 | vim.g.puppeteer_disable_filetypes = {} 131 | < 132 | 133 | 134 | [!TIP] You can also use `PuppeteerToggle` to temporarily disable the plugin for 135 | the current buffer, if a specific lua string is giving you trouble. 136 | 137 | CREDITS *nvim-puppeteer-nvim-puppeteer--credits* 138 | 139 | In my day job, I am a sociologist studying the social mechanisms underlying the 140 | digital economy. For my PhD project, I investigate the governance of the app 141 | economy and how software ecosystems manage the tension between innovation and 142 | compatibility. If you are interested in this subject, feel free to get in 143 | touch. 144 | 145 | - Website 146 | - Mastodon 147 | - ResearchGate 148 | - LinkedIn 149 | 150 | 151 | 152 | Generated by panvimdoc 153 | 154 | vim:tw=78:ts=8:noet:ft=help:norl: 155 | --------------------------------------------------------------------------------