├── .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 |
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 |
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 |
--------------------------------------------------------------------------------