Repository: uga-rosa/translate.nvim Branch: main Commit: 1a841e56407b Files: 67 Total size: 129.4 KB Directory structure: gitextract_mwzms6v0/ ├── .github/ │ └── ISSUE_TEMPLATE/ │ └── bug_report.yml ├── .gitignore ├── LICENSE ├── README.md ├── doc/ │ └── translate-nvim.txt ├── lua/ │ └── translate/ │ ├── command.lua │ ├── config.lua │ ├── init.lua │ ├── kit/ │ │ ├── Async/ │ │ │ ├── AsyncTask.lua │ │ │ ├── AsyncTask.spec.lua │ │ │ ├── init.lua │ │ │ └── init.spec.lua │ │ ├── Cache.lua │ │ ├── Cache.spec.lua │ │ ├── Config.lua │ │ ├── Config.spec.lua │ │ ├── LSP/ │ │ │ ├── Position.lua │ │ │ ├── Position.spec.lua │ │ │ ├── Range.lua │ │ │ └── Range.spec.lua │ │ ├── Lua/ │ │ │ ├── TreeSitter.lua │ │ │ ├── TreeSitter.spec.lua │ │ │ ├── init.lua │ │ │ └── init.spec.lua │ │ ├── Vim/ │ │ │ ├── Buffer.lua │ │ │ ├── Buffer.spec.lua │ │ │ ├── Highlight.lua │ │ │ ├── Highlight.spec.lua │ │ │ ├── Keymap.lua │ │ │ ├── Keymap.spec.lua │ │ │ ├── Syntax.lua │ │ │ └── Syntax.spec.lua │ │ ├── init.lua │ │ └── init.spec.lua │ ├── preset/ │ │ ├── command/ │ │ │ ├── deepl.lua │ │ │ ├── deepl_free.lua │ │ │ ├── deepl_pro.lua │ │ │ ├── google.lua │ │ │ └── translate_shell.lua │ │ ├── output/ │ │ │ ├── floating.lua │ │ │ ├── insert.lua │ │ │ ├── register.lua │ │ │ ├── replace.lua │ │ │ └── split.lua │ │ ├── parse_after/ │ │ │ ├── deepl.lua │ │ │ ├── deepl_free.lua │ │ │ ├── deepl_pro.lua │ │ │ ├── google.lua │ │ │ ├── head.lua │ │ │ ├── no_handle.lua │ │ │ ├── oneline.lua │ │ │ ├── rate.lua │ │ │ ├── translate_shell.lua │ │ │ └── window.lua │ │ └── parse_before/ │ │ ├── concat.lua │ │ ├── natural.lua │ │ ├── no_handle.lua │ │ └── trim.lua │ └── util/ │ ├── comment.lua │ ├── context.lua │ ├── replace.lua │ ├── select.lua │ ├── utf8.lua │ └── util.lua ├── plugin/ │ └── translate.lua ├── stylua.toml └── utils/ └── minimal.vim ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: Bug Report description: Report a problem in translate.nvim labels: [bug] body: - type: checkboxes id: not-question attributes: label: Not a question options: - label: This is not a question. If you have a question, use [discussions](https://github.com/uga-rosa/translate.nvim/discussions). required: true - type: textarea attributes: label: "Description" description: "Describe in detail what happens" validations: required: true - type: textarea attributes: label: "Environments" description: "information such as OS and neovim version" validations: required: true - type: textarea attributes: label: "Minimal reproducible full config" description: | You must provide a working config based on [this](https://github.com/uga-rosa/translate.nvim/blob/main/utils/minimal.vim). The config file should be complete, not partial, and can be used alone to reproduce bugs. **Don't use packer.nvim for this.** validations: required: true - type: textarea attributes: label: "Steps to reproduce" description: "Full reproduction steps. Include a sample file if your issue relates to a specific filetype." validations: required: true - type: textarea attributes: label: "Expected behavior" description: "A description of the behavior you expected." validations: required: true - type: textarea attributes: label: "Actual behavior" description: "A description of the actual behavior." validations: required: true - type: textarea attributes: label: "Additional context" description: "Any other relevant information" ================================================ FILE: .gitignore ================================================ /doc/tags ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2021 uga-rosa Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # translate.nvim ![demo](https://user-images.githubusercontent.com/82267684/158013979-52c8ca49-84e1-4ca0-bf30-b8165cca9135.gif) # Features - You can use any command you like for translation. - Google translate API (default) - [translate-shell](https://github.com/soimort/translate-shell) - [DeepL API Pro/Free](https://www.deepl.com/en/docs-api/) - The results of the translation can be output in a variety of ways. - Floating window - Split window - Insert to the current buffer - Replace the original text - Set the register - The translation command and output method can be specified as command arguments. - In addition to the above presets, you can add your own functions. # Requirements - neovim 0.8+ The default Google Translate requires nothing but [curl](https://curl.se/). If you use [translate-shell](https://github.com/soimort/translate-shell), you need to install `trans` command. If you use [DeepL API Pro/Free](https://www.deepl.com/en/docs-api/), you need the authorization key for DeepL API Pro/Free. In addition, you need curl to send the request. # Quick start ## Install With any plugin manager you like (e.g. [vim-plug](https://github.com/junegunn/vim-plug), [packer.nvim](https://github.com/wbthomason/packer.nvim), [dein.vim](https://github.com/Shougo/dein.vim)) ## Setup This plugin has default settings, so there is no need to call setup if you want to use it as is. This is my setting. ```lua vim.g.deepl_api_auth_key = "MY_AUTH_KEY" require("translate").setup({ default = { command = "deepl_pro", }, preset = { output = { split = { append = true, }, }, }, }) ``` See help for available options. ## Command This plugin provides `:Translate`. I put the quote from the help in the fold.
:Translate
:[range]Translate {target-lang} [{-options}...] {target-lang}: Required. The language into which the text should be translated. The format varies depending on the external command used. |:Translate| can take |:range|. |v|, |V| and |CTRL-V| are supported. If it was not given, |:Translate| treats current cursor line. available options: - '-source=' The language of the text to be translated. - '-parse_before=' The functions to format texts of selection. You can use a comma-separated string. If omitted, |translate-nvim-option-default-parse-before|. - '-command=' The extermal command to use translation. If omitted, |translate-nvim-option-default-command| is used. - '-parse_after=' The functions to format the result of extermal command. You can use a comma-separated string. If omitted, |translate-nvim-option-default-parse-after|. - '-output=' The function to pass the translation result. If omitted, |translate-nvim-option-default-output|. - '-comment' Special option, used as a flag. If this flag is set and the cursor is over a comment, whole comment is treated as a selection. Use for mapping. If you cannot use it, you must change the format with nmap and xmap. nnoremap me Translate EN xnoremap me Translate EN Another way. nnoremap me :Translate EN xnoremap me :Translate EN
# Translate the word under the cursor You can use this mapping. ```vim nnoremap tw viw:Translate ZH ``` ================================================ FILE: doc/translate-nvim.txt ================================================ *translate-nvim.txt* Use external translate command in nvim ============================================================================== Contents *translate-nvim-contents* Introduction |translate-nvim-introduction| Command |translate-nvim-command| Setup |translate-nvim-setup| Option |translate-nvim-option| - default |translate-nvim-option-default| - preset |translate-nvim-option-preset| - parse_before |translate-nvim-option-parse-before| - command |translate-nvim-option-command| - parse_after |translate-nvim-option-parse-after| - output |translate-nvim-option-output| - replace_symbols |translate-nvim-option-replace-symbols| - silent |translate-nvim-option-silent| preset |translate-nvim-preset| - parse_before |translate-nvim-preset-parse-before| - natural |translate-nvim-preset-parse-before-natural| - trim |translate-nvim-preset-parse-before-trim| - concat |translate-nvim-preset-parse-before-concat| - no_handle |translate-nvim-preset-parse-before-no-handle| - command |translate-nvim-preset-command| - google |translate-nvim-preset-command-google| - translate_shell |translate-nvim-preset-command-translate-shell| - deepl_free |translate-nvim-preset-command-deepl-free| - deepl_pro |translate-nvim-preset-command-deepl-pro| - parse_after |translate-nvim-preset-parse-after| - oneline |translate-nvim-preset-parse-after-oneline| - head |translate-nvim-preset-parse-after-head| - rate |translate-nvim-preset-parse-after-rate| - window |translate-nvim-preset-parse-after-window| - no_handle |translate-nvim-preset-parse-after-no-handle| - translate_shell |translate-nvim-preset-parse-after-translate-shell| - deepl |translate-nvim-preset-parse-after-deepl| - output |translate-nvim-preset-output| - split |translate-nvim-preset-output-split| - floating |translate-nvim-preset-output-floating| - insert |translate-nvim-preset-output-insert| - replace |translate-nvim-preset-output-replace| - register |translate-nvim-preset-output-register| Variables |translate-nvim-variables| ============================================================================== Introduction *translate-nvim-introduction* *translate.nvim* *translate-nvim* translate.nvim ~ |translate.nvim| is a plugin for nvim that allows you to translate the selection with any external command and handle the result as you like. It provides |:Translate| for it. Requirement: - neovim >= 0.7 - curl (for |translate-nvim-preset-command-google|, |translate-nvim-preset-command-deepl-free|, and |translate-nvim-preset-command-deepl-pro|) - DeepL API authorization key (for |translate-nvim-preset-command-deepl-free| and |translate-nvim-preset-command-deepl-pro|) - trans (for |translate-nvim-preset-command-translate-shell|) ============================================================================== Command *translate-nvim-command* *:Translate* :[range]Translate {target-lang} [{-options}...] {target-lang}: Required. The language into which the text should be translated. The format varies depending on the external command used. |:Translate| can take |:range|. |v|, |V| and |CTRL-V| are supported. If it was not given, |:Translate| treats current cursor line. available options: - '-source=' The language of the text to be translated. - '-parse_before=' The functions to format texts of selection. You can use a comma-separated string. If omitted, |translate-nvim-option-default-parse-before|. - '-command=' The extermal command to use translation. If omitted, |translate-nvim-option-default-command| is used. - '-parse_after=' The functions to format the result of extermal command. You can use a comma-separated string. If omitted, |translate-nvim-option-default-parse-after|. - '-output=' The function to pass the translation result. If omitted, |translate-nvim-option-default-output|. - '-comment' Special option, used as a flag. If this flag is set and the cursor is over a comment, whole comment is treated as a selection. If mapping |:Translate|, You can use ||. > nnoremap me Translate EN xnoremap me Translate EN < If you cannot use it, you must change the format with nmap and xmap. > nnoremap me :Translate EN xnoremap me :Translate EN < ============================================================================== Setup *translate-nvim-setup* The options are set through the setup function. See |translate-nvim-option| to check available options. > require("translate").setup({ default = { command = "deepl_free", output = "floating", }, preset = { output = { insert = { base = "top", off = -1, }, }, }, }) < ============================================================================== Option *translate-nvim-option* *translate-nvim-option-default* default ~ table 'parse_before', 'command', 'parse_after' and 'output' used by |:Translate|. See |translate-nvim-preset|, to check the presets provided by |translate-nvim|. *translate-nvim-option-default-parse-before* parse_before ~ string default: 'trim,natural' See |translate-nvim-preset-parse-before-trim| and |translate-nvim-preset-parse-before-concat|. *translate-nvim-option-default-command* command ~ string default: 'google' See |translate-nvim-preset-command-google|. *translate-nvim-option-default-parse-after* parse_after ~ string default: 'head' See |translate-nvim-preset-parse-after-remove-newline| and |translate-nvim-preset-parse-after-floating|. *translate-nvim-option-default-output* output ~ string default: 'floating' See |translate-nvim-preset-output-floating|. *translate-nvim-option-preset* preset ~ Options passed to the presets. *translate-nvim-option-preset-parse-before* parse_before ~ *translate-nvim-option-preset-parse-before-natural* natural ~ table default: { lang_abbr = {}, end_marks = {}, start_marks = {}, } Table 'lang_abbr' for converting the '-source' option of the command to the language, list 'end_marks' of the end-of-sentence characters pattern (vim regular expression, |/\V|), and list 'start_marks' of the start-of-sentence characters pattern (same 'end_marks'). Use lowercase for language names and their abbreviations. For example, in English, which is defined by default. > { lang_abbr = { en = "english", eng = "english", }, end_marks = { english = { ".", "?", "!", ":", ";", }, }, } < Only 'english', 'japanese', and 'chinese' have rules defined by default. Other languages can be defined by yourself or PRs are welcome! *translate-nvim-option-preset-parse-before-trim* trim ~ nil There are currently no options for 'trim'. *translate-nvim-option-preset-parse-before-concat* concat ~ table default: { sep = " " } Sets the delimiter used to join lines. *translate-nvim-option-preset-command* command ~ *translate-nvim-option-preset-command-google* google ~ table default: { args = {} } Set the extra arguments to be passed to the 'curl' command. *translate-nvim-option-preset-command-translate-shell* translate_shell ~ table default: { args = {} } Set the extra arguments to be passed to the 'trans' command. *translate-nvim-option-preset-command-deepl-free* deepl_free ~ table default: { args = {} } Set the extra arguments to be passed to the 'curl' command. *translate-nvim-option-preset-command-deepl-pro* deepl_pro ~ table default: { args = {} } Set the extra arguments to be passed to the 'curl' command. *translate-nvim-option-preset-parse-after* parse_after ~ *translate-nvim-option-preset-parse-after-oneline* *translate-nvim-option-preset-parse-after-head* *translate-nvim-option-preset-parse-after-rate* *translate-nvim-option-preset-parse-after-translate-shell* *translate-nvim-option-preset-parse-after-deepl* oneline, head, rate, translate_shell, deepl ~ nil There are currently no options for these. *translate-nvim-option-preset-parse-after-window* window ~ table: default: { width = 0.8 } The 'width' is a percentage of the current window width. If it is greater than 1, that value, not the percentage, is used as the fixed value. *translate-nvim-option-preset-output* output ~ *translate-nvim-option-preset-output-split* split ~ table default: { position = "top", min_size = 5 max_size = 0.5 name = "translate://output", filetype = "translate", append = false, } The 'position' is where the result will be placed: 'top' or 'bottom'. The 'min_size' and 'max_size' are buffer size limits. The buffer size depends on the number of lines of the translation result, but you can set an upper and lower limit. If it is less than 1, it is a percentage of the current window. In the event of a conflict, min_size takes precedence. The 'name' and 'filetype' is set to the split buffer. If 'append' is true, without deleting the previous translation result, the current one will be added to the last line. *translate-nvim-option-preset-output-floating* floating ~ table default: { relative = "cursor", style = "minimal", width = nil, height = nil, row = 1, col = 1, border = "single", filetype = "translate", zindex = 50, } The option passed as the 3rd argument of |nvim_open_win()|. The 'width' and 'height' are automatically calculated from the received array. *translate-nvim-option-preset-output-insert* insert ~ table default: { base = 'bottom', off = 0, } Where to insert the translation result. If 'base' is 'top', the first line of the selection is used as the base, else if 'bottom', the last of line the selection. Finally, add 'off' to the base. For example, with the default, it will be inserted just bellow the selection. *translate-nvim-option-preset-output-replace* replace ~ You can choose the behavior of replace: 'head' respects the starting position and the original position; 'rate' calculates and distributes a percentage of the length of each original line. *translate-nvim-option-preset-output-register* register ~ table default: { name = vim.v.register } Sets the translation result to the register specified by 'name'. Users who set |clipboard| may want to check |register-variable| before changing this option. *translate-nvim-option-parse-before* parse_before ~ table default: {} You can set any function you want to use for formatting selection. Set tables with the value which has as 'cmd' key a function that returns the command and arguments. Check 'lua/translate/preset/parse_before' for details. *translate-nvim-option-command* command ~ table default: {} You can set any external command you want to use for translation. Set tables with the value which has as 'cmd' key a function that returns the command and arguments. Check 'lua/translate/preset/command' for details. *translate-nvim-option-parse-after* parse_after ~ table default: {} You can set functions to format the result of the translation. Set tables with the value which has as 'cmd' key a function. Check 'lua/translate/preset/parse_after' for details. *translate-nvim-option-output* output ~ table default: {} You can set functions to be passed the result of the translation. Set tables with the value which has as 'cmd' key a function. Check 'lua/translate/preset/output' for details. *translate-nvim-option-replace-symbols* replace_symbols ~ table default: { translate_shell = { ["="] = "{@E@}", ["#"] = "{@S@}", ["/"] = "{@C@}", }, deepl_free = {}, deepl_pro = {}, google = {}, } This plugin escapes special strings for successful translation. This is its corresponding dictionary. For example, translate_shell has problems with '=' being translated into the strange string 'u003d', or failing to translate strings that begin with '/'. Therefore, we temporarily convert the symbols to special symbols such as '{{@E@}}' before performing the translation, and then restore the symbols in the translation result for normal translation. *translate-nvim-option-silent* silent ~ boolean default: false If true, the 'Translate success/failed' messages will be disabled. ============================================================================== Preset *translate-nvim-preset* The following is a list of commands, parsing functions, and output methods provided by this plugin. *translate-nvim-preset-parse-before* parse_before ~ A set of functions that take an array of lines of text from a selection and process them into a string that is eventually passed to the translation command. The second and subsequent functions receive the return value of the previous function. *translate-nvim-preset-parse-before-natural* natural ~ Separates selection with a blank line or when the start/end of a line is the start/end of a sentence. To use it, pass the 'source' option to the command and tell it the original language. By default, this grammar rule is defined only for English, Japanese and Chinese. *translate-nvim-preset-parse-before-trim* trim ~ Execute |vim.trim()| on each line. *translate-nvim-preset-parse-before-concat* concat ~ Concatenates selection into a single string using a delimiter. The delimiter is by default. If you want to change it, use |translate-nvim-option-preset-parse-before-concat|. *translate-nvim-preset-parse-before-no-handle* no_handle ~ If you don't want to adjust anything, use this. *translate-nvim-preset-command* command ~ API/External commands used for translation. 'curl' is required except for translate_shell. *translate-nvim-preset-command-google* google ~ Use Google Translate API via GAS. There is nothing for you to prepare. *translate-nvim-preset-command-translate-shell* translate_shell ~ Use translate-shell. You need to install 'trans' command. *translate-nvim-preset-command-deepl-free* deepl_free ~ Use DeepL API Free Set your DeepL API authorization key to |g:deepl_api_auth_key|. *translate-nvim-preset-command-deepl-pro* deepl_pro ~ Use DeepL API Pro What you need is the same as |translate-nvim-preset-command-deepl-free|. *translate-nvim-preset-parse-after* parse_after ~ A set of functions that take a result of translation and process them into a string that is eventually passed to the output. The second and subsequent functions receive the return value of the previous function. *translate-nvim-preset-parse-after-oneline* oneline ~ Summarize the results on a single line. It is intended to be used with 'split', 'insert', and 'replace'. It is deprecated for use in 'floating' as it may not fit on the screen. *translate-nvim-preset-parse-after-head* head ~ Splits the translation result to fit the display width of the original text in the selected area. We cannot guarantee the number of characters in the last line because the number of characters changes before and after the translation. It is intended to be used with 'split', 'insert', 'replace', and 'window'. *translate-nvim-preset-parse-after-rate* rate ~ Divides the translation result by the percentage of each line of the original text display width. It is intended to be used with 'split', 'insert', 'replace', and 'window'. *translate-nvim-preset-parse-after-window* window ~ Splits the text to fit the specified window width. Default is 0.8 (percentage of the current window). Use |translate-nvim-option-preset-parse-after-window| to change it. It is intended to be used with 'split', 'insert', 'replace', and 'window'. *translate-nvim-preset-parse-after-no-handle* no_handle ~ If you don't want to adjust anything, use this. *translate-nvim-preset-parse-after-translate-shell* translate_shell ~ If 'command' is 'translate_shell', this parser is added automatically. In other words, you do not need to specify this unless you want to use only this. Split by line breaks in the translation result or remove extra line break characters at the end of it. *translate-nvim-preset-parse-after-deepl* deepl ~ If 'command' is 'deepl_pro/free', this parser is added automatically. In other words, you do not need to specify this unless you want to use only this. DeepL API returns the response in json format, which is parsed and the text of the translation result is taken. Use vim.json.decode (neovim 0.6.0+) or |json_decode| *translate-nvim-preset-output* output ~ Function passed the result of translation. *translate-nvim-preset-output-split* split ~ Split the window and output the result to it. By default, the previous translation result is deleted each time. If you want to keep it, use the option. See |translate-nvim-option-preset-output-split|. *translate-nvim-preset-output-floating* floating ~ Display the result in a floating window. See |translate-nvim-option-preset-output-floating|. *translate-nvim-preset-output-insert* insert ~ Insert the result into the current buffer. By default, it is inserted just below the selection. See |translate-nvim-option-preset-output-insert|. *translate-nvim-preset-output-replace* replace ~ Replace the original text with the result. See |translate-nvim-option-preset-output-replace|. *translate-nvim-preset-output-register* register ~ Set the result to the register. See |translate-nvim-option-preset-output-register|. ============================================================================== Variables *translate-nvim-variables* *g:deepl_api_auth_key* g:deepl_api_auth_key ~ Authentication key for DeepL API. vim:tw=78:ts=8:noet:ft=help:norl: ================================================ FILE: lua/translate/command.lua ================================================ local fn = vim.fn local config = require("translate.config") local M = {} local modes = { "parse_before", "command", "parse_after", "output", "source", "comment", } ---@param arglead string #The leading portion of the argument currently being completed on ---@param cmdline string #The entire command line ---@param _ number #the cursor position in it (byte index) ---@return string[]? function M.get_complete_list(arglead, cmdline, _) local mode if not vim.startswith(arglead, "-") then mode = "target" elseif arglead:find("^%-.*=") then mode = arglead:match("^%-(.*)=") else return modes end if vim.tbl_contains({ "parse_before", "command", "parse_after", "output" }, mode) then return config.get_keys(mode) elseif vim.tbl_contains({ "source", "target" }, mode) then local command = cmdline:match("-command=(%S+)") command = command or config.get("default").command local module = config.config.command[command] or config._preset.command[command] if module and module.complete_list then return module.complete_list(mode == "target") end end end ---@param cb fun(mode: string, fargs: string[]) function M.create_command(cb) vim.api.nvim_create_user_command("Translate", function(opts) -- If range is 0, not given, it has been called from normal mode, or visual mode with `` mapping. -- Otherwise it must have been called from visual mode. local mode = opts.range == 0 and fn.mode() or fn.visualmode() cb(mode, opts.fargs) end, { range = 0, nargs = "+", complete = M.get_complete_list, }) end return M ================================================ FILE: lua/translate/config.lua ================================================ local M = {} M._preset = { parse_before = { natural = require("translate.preset.parse_before.natural"), trim = require("translate.preset.parse_before.trim"), concat = require("translate.preset.parse_before.concat"), no_handle = require("translate.preset.parse_before.no_handle"), }, command = { translate_shell = require("translate.preset.command.translate_shell"), deepl_free = require("translate.preset.command.deepl_free"), deepl_pro = require("translate.preset.command.deepl_pro"), google = require("translate.preset.command.google"), }, parse_after = { oneline = require("translate.preset.parse_after.oneline"), head = require("translate.preset.parse_after.head"), rate = require("translate.preset.parse_after.rate"), window = require("translate.preset.parse_after.window"), no_handle = require("translate.preset.parse_after.no_handle"), translate_shell = require("translate.preset.parse_after.translate_shell"), deepl_free = require("translate.preset.parse_after.deepl_free"), deepl_pro = require("translate.preset.parse_after.deepl_pro"), google = require("translate.preset.parse_after.google"), }, output = { floating = require("translate.preset.output.floating"), split = require("translate.preset.output.split"), insert = require("translate.preset.output.insert"), replace = require("translate.preset.output.replace"), register = require("translate.preset.output.register"), }, } M.config = { default = { parse_before = "trim,natural", command = "google", parse_after = "head", output = "floating", }, parse_before = {}, command = {}, parse_after = {}, output = {}, preset = { parse_before = { natural = { lang_abbr = {}, end_marks = {}, start_marks = {}, }, concat = { sep = " ", }, }, command = { google = { args = {}, }, translate_shell = { args = {}, }, deepl_free = { args = {}, }, deepl_pro = { args = {}, }, }, parse_after = { window = { width = 0.8, }, }, output = { floating = { relative = "cursor", style = "minimal", row = 1, col = 1, border = "single", filetype = "translate", zindex = 50, }, split = { position = "top", min_size = 3, max_size = 0.3, name = "translate://output", filetype = "translate", append = false, }, insert = { base = "bottom", off = 0, }, register = { name = vim.v.register, }, }, }, silent = false, replace_symbols = { translate_shell = { ["="] = "{@E@}", ["#"] = "{@S@}", ["/"] = "{@C@}", ["\\n"] = "{@N@}", }, deepl_free = {}, deepl_pro = {}, google = {}, }, } ---@param opt table function M.setup(opt) M.config = vim.tbl_deep_extend("force", M.config, opt) end ---@param name string ---@return boolean|table function M.get(name) return M.config[name] end ---@param mode string ---@param name string ---@return fun(lines: string[], command_args: table) ---@return string function M.get_func(mode, name) name = name or M.config.default[mode] local module = M.config[mode][name] or M._preset[mode][name] if module and module.cmd then return module.cmd, name else error(("Invalid name of %s: %s"):format(module, name)) end end ---@param mode string ---@param names string ---@return fun(text: string, command_args: table)[] ---@return string[] function M.get_funcs(mode, names) names = names or M.config.default[mode] names = vim.split(names, ",") local modules = {} for _, name in ipairs(names) do local module = M.config[mode][name] or M._preset[mode][name] if module and module.cmd then table.insert(modules, module.cmd) else error(("Invalid name of %s: %s"):format(mode, name)) end end return modules, names end ---For completion of command ':Translate' ---@param mode string ---@return string[] function M.get_keys(mode) local keys = vim.tbl_keys(M.config[mode]) keys = vim.list_extend(keys, vim.tbl_keys(M._preset[mode])) return keys end return M ================================================ FILE: lua/translate/init.lua ================================================ local luv = vim.loop local config = require("translate.config") local replace = require("translate.util.replace") local select = require("translate.util.select") local util = require("translate.util.util") local create_command = require("translate.command").create_command local M = {} ---@param mode string ---@param args string[] function M.translate(mode, args) args = M._parse_args(args) local pos = select.get(args, mode) if #pos == 0 then vim.notify("Selection could not be recognized.") return end M._translate(pos, args) end ---@param opts string[] ---@return table function M._parse_args(opts) local args = {} for _, opt in ipairs(opts) do local name, arg = opt:match("-([a-z_]+)=(.*)") -- e.g. '-parse_after=head' if not name then name = opt:match("-(%l+)") -- for '-comment' if name then arg = true else -- '{target-lang}' name = "target" arg = opt end end args[name] = arg end return args end local function pipes() local stdin = luv.new_pipe(false) local stdout = luv.new_pipe(false) local stderr = luv.new_pipe(false) return { stdin, stdout, stderr } end local function set_to_top(tbl, elem) if tbl[1] ~= elem then table.insert(tbl, 1, elem) end end ---@param pos positions ---@param cmd_args table function M._translate(pos, cmd_args) local parse_before = config.get_funcs("parse_before", cmd_args.parse_before) local command, command_name = config.get_func("command", cmd_args.command) local parse_after = config.get_funcs("parse_after", cmd_args.parse_after) local output = config.get_func("output", cmd_args.output) replace.set_command_name(command_name) set_to_top(parse_before, replace.before) set_to_top(parse_after, replace.after) local after_process = config._preset.parse_after[command_name] if after_process and after_process.cmd then set_to_top(parse_after, after_process.cmd) end local lines = M._selection(pos) pos._lines_selected = lines ---@type string[] lines = M._run(parse_before, lines, pos, cmd_args) if not pos._group then pos._group = util.seq(#lines) end local cmd, args = command(lines, cmd_args) local stdio = pipes() local handle handle = luv.spawn(cmd, { args = args, stdio = stdio }, function(code) if not config.get("silent") then if code == 0 then print("Translate success") else print("Translate failed") end end handle:close() end) if not handle then return end luv.read_start( stdio[2], vim.schedule_wrap(function(err, result) assert(not err, err) if result then result = M._run(parse_after, result, pos) output(result, pos) end end) ) end ---@param pos positions ---@return string[] function M._selection(pos) local lines = {} for i, line in ipairs(pos._lines) do local col = pos[i].col table.insert(lines, line:sub(col[1], col[2])) end return lines end ---@generic T ---@param functions function[] ---@param arg `T` ---@param pos positions ---@param cmd_args? string[] ---@return T function M._run(functions, arg, pos, cmd_args) for _, func in ipairs(functions) do arg = func(arg, pos, cmd_args) end return arg end ---@param opt table function M.setup(opt) config.setup(opt) create_command(M.translate) vim.g.loaded_translate_nvim = true end return M ================================================ FILE: lua/translate/kit/Async/AsyncTask.lua ================================================ local Lua = require("___plugin_name___.kit.Lua") ---@class ___plugin_name___.kit.Async.AsyncTask: { value: T } ---@field private value T ---@field private status ___plugin_name___.kit.Async.AsyncTask.Status ---@field private chained boolean ---@field private children (fun(): any)[] local AsyncTask = {} AsyncTask.__index = AsyncTask ---@alias ___plugin_name___.kit.Async.AsyncTask.Status integer AsyncTask.Status = {} AsyncTask.Status.Pending = 0 AsyncTask.Status.Fulfilled = 1 AsyncTask.Status.Rejected = 2 ---Handle unhandled rejection. ---@param err any function AsyncTask.on_unhandled_rejection(err) error(err) end ---Return the value is AsyncTask or not. ---@param value any ---@return boolean function AsyncTask.is(value) return getmetatable(value) == AsyncTask end ---Resolve all tasks. ---@param tasks any[] ---@return ___plugin_name___.kit.Async.AsyncTask function AsyncTask.all(tasks) return AsyncTask.new(function(resolve, reject) local values = {} local count = 0 for i, task in ipairs(tasks) do AsyncTask.resolve(task) :next(function(value) values[i] = value count = count + 1 if #tasks == count then resolve(values) end end) :catch(reject) end end) end ---Create resolved AsyncTask. ---@param v any ---@return ___plugin_name___.kit.Async.AsyncTask function AsyncTask.resolve(v) if AsyncTask.is(v) then return v end return AsyncTask.new(function(resolve) resolve(v) end) end ---Create new AsyncTask. ---@NOET: The AsyncTask has similar interface to JavaScript Promise but the AsyncTask can be worked as synchronous. ---@param v any ---@return ___plugin_name___.kit.Async.AsyncTask function AsyncTask.reject(v) if AsyncTask.is(v) then return v end return AsyncTask.new(function(_, reject) reject(v) end) end ---Create new async task object. ---@generic T ---@param runner fun(resolve: fun(value: T), reject: fun(err: any)) function AsyncTask.new(runner) local self = setmetatable({}, AsyncTask) self.gc = Lua.gc(function() if self.status == AsyncTask.Status.Rejected then if not self.chained then AsyncTask.on_unhandled_rejection(self.value) end end end) self.value = nil self.status = AsyncTask.Status.Pending self.chained = false self.children = {} local ok, err = pcall(function() runner(function(res) if self.status ~= AsyncTask.Status.Pending then return end self.status = AsyncTask.Status.Fulfilled self.value = res for _, c in ipairs(self.children) do c() end end, function(err) if self.status ~= AsyncTask.Status.Pending then return end self.status = AsyncTask.Status.Rejected self.value = err for _, c in ipairs(self.children) do c() end end) end) if not ok then self.status = AsyncTask.Status.Rejected self.value = err for _, c in ipairs(self.children) do c() end end return self end ---Sync async task. ---@NOTE: This method uses `vim.wait` so that this can't wait the typeahead to be empty. ---@param timeout? number ---@return any function AsyncTask:sync(timeout) vim.wait(timeout or 24 * 60 * 60 * 1000, function() return self.status ~= AsyncTask.Status.Pending end, 0) if self.status == AsyncTask.Status.Rejected then error(self.value) end if self.status ~= AsyncTask.Status.Fulfilled then error("AsyncTask:sync is timeout.") end return self.value end ---Register next step. ---@param on_fulfilled fun(value: any): any function AsyncTask:next(on_fulfilled) return self:_dispatch(on_fulfilled, function(err) error(err) end) end ---Register catch step. ---@param on_rejected fun(value: any): any ---@return ___plugin_name___.kit.Async.AsyncTask function AsyncTask:catch(on_rejected) return self:_dispatch(function(value) return value end, on_rejected) end ---Dispatch task state. ---@param on_fulfilled fun(value: any): any ---@param on_rejected fun(err: any): any ---@return ___plugin_name___.kit.Async.AsyncTask function AsyncTask:_dispatch(on_fulfilled, on_rejected) self.chained = true local function dispatch(resolve, reject) if self.status == AsyncTask.Status.Fulfilled then local res = on_fulfilled(self.value) if AsyncTask.is(res) then res:next(resolve, reject) else resolve(res) end else local res = on_rejected(self.value) if AsyncTask.is(res) then res:next(resolve, reject) else resolve(res) end end end if self.status == AsyncTask.Status.Pending then return AsyncTask.new(function(resolve, reject) table.insert(self.children, function() dispatch(resolve, reject) end) end) end return AsyncTask.new(dispatch) end return AsyncTask ================================================ FILE: lua/translate/kit/Async/AsyncTask.spec.lua ================================================ local AsyncTask = require("___plugin_name___.kit.Async.AsyncTask") describe("kit.Async", function() local once = function(fn) local done = false return function(...) if done then error("already called") end done = true return fn(...) end end it("should work AsyncTask:{next/catch}", function() -- first. local one_task = AsyncTask.new(once(function(resolve) vim.schedule(function() resolve(1) end) end)) assert.are.equals(one_task:sync(), 1) -- next with return value. local two_task = one_task:next(once(function(value) return value + 1 end)) assert.are.equals(two_task:sync(), 2) -- next with return AsyncTask. local three_task = two_task:next(once(function(value) return AsyncTask.new(function(resolve) vim.schedule(function() resolve(value + 1) end) end) end)) assert.are.equals(three_task:sync(), 3) -- throw error. local err_task = three_task:next(once(function() error("error") end)) local _, err = pcall(function() return err_task:sync() end) assert.are_not.equals(string.match(err, "error$"), nil) -- skip rejected task's next. local steps = {} local catch_task = err_task :next(once(function() table.insert(steps, 1) end)) :next(once(function() table.insert(steps, 2) end)) :catch(function() return "catch" end) :next(function(value) table.insert(steps, 3) return value end) assert.are.same(steps, { 3 }) assert.are.equals(catch_task:sync(), "catch") end) it("should throw timeout error", function() local task = AsyncTask.new(function(resolve) vim.defer_fn(resolve, 500) end) local ok = pcall(function() return task:sync(100) end) assert.is_false(ok) end) it("should work AsyncTask.all", function() local now = vim.loop.now() local values = AsyncTask.all({ AsyncTask.new(function(resolve) vim.defer_fn(function() resolve(1) end, 300) end), AsyncTask.new(function(resolve) vim.defer_fn(function() resolve(2) end, 200) end), AsyncTask.new(function(resolve) vim.defer_fn(function() resolve(3) end, 100) end), }):sync() assert.are.same(values, { 1, 2, 3 }) assert.is_true((vim.loop.now() - now) - 300 < 10) end) it("should work AsyncTask.on_unhandled_rejection", function() local object local called = false AsyncTask.on_unhandled_rejection = function() called = true end -- has no catched task. object = AsyncTask.new(function() error("error") end) object = nil called = false collectgarbage("collect") assert.are.equals(object, nil) assert.are.equals(called, true) -- has no catched task. object = AsyncTask.new(function() error("error") end):catch(function() -- ignore end) object = nil called = false collectgarbage("collect") assert.are.equals(object, nil) assert.are.equals(called, false) -- has no catched task. object = AsyncTask.new(function() error("error") end):next(function() -- ignore end) object = nil called = false collectgarbage("collect") assert.are.equals(object, nil) assert.are.equals(called, true) end) end) ================================================ FILE: lua/translate/kit/Async/init.lua ================================================ local AsyncTask = require("___plugin_name___.kit.Async.AsyncTask") _G.__kit__ = _G.__kit__ or {} _G.__kit__.Async = _G.__kit__.Async or {} _G.__kit__.Async.threads = _G.__kit__.Async.threads or {} local Async = {} ---Run async function immediately. ---@param runner fun() ---@param ... any ---@return ___plugin_name___.kit.Async.AsyncTask function Async.run(runner, ...) return Async.async(runner)(...) end ---Create async function. ---@param runner fun() ---@return fun(): ___plugin_name___.kit.Async.AsyncTask function Async.async(runner) return function(...) local args = { ... } return AsyncTask.new(function(resolve, reject) local thread = coroutine.create(runner) _G.__kit__.Async.threads[thread] = true local function next_step(ok, v) if coroutine.status(thread) == "dead" then _G.__kit__.Async.threads[thread] = nil if not ok then return reject(v) end return AsyncTask.resolve(v):next(resolve):catch(reject) end AsyncTask.resolve(v) :next(function(...) next_step(coroutine.resume(thread, ...)) end) :catch(function(...) next_step(coroutine.resume(thread, ...)) end) end next_step(coroutine.resume(thread, unpack(args))) end) end end ---Await async task. ---@param task ___plugin_name___.kit.Async.AsyncTask ---@return any function Async.await(task) if not _G.__kit__.Async.threads[coroutine.running()] then error("`Async.await` must be called in async function.") end return coroutine.yield(AsyncTask.resolve(task)) end return Async ================================================ FILE: lua/translate/kit/Async/init.spec.lua ================================================ local Async = require("___plugin_name___.kit.Async") local AsyncTask = require("___plugin_name___.kit.Async.AsyncTask") local async = Async.async local await = Async.await describe("kit.Async", function() it("should work like JavaScript Promise", function() local multiply = async(function(v) return AsyncTask.new(function(resolve) vim.schedule(function() resolve(v * v) end) end) end) local num = async(function() local num = 2 num = await(multiply(num)) num = await(multiply(num)) return num end)():sync() assert.are.equal(num, 16) end) end) ================================================ FILE: lua/translate/kit/Cache.lua ================================================ ---Create cache key. ---@private ---@param key string[]|string ---@return string local function _key(key) if type(key) == "table" then return table.concat(key, ":") end return key end ---@class ___plugin_name___.kit.Cache ---@field private keys table ---@field private entries table local Cache = {} ---Create new cache instance. function Cache.new() local self = setmetatable({}, { __index = Cache }) self.keys = {} self.entries = {} return self end ---Get cache entry. ---@param key string[]|string ---@return any function Cache:get(key) return self.entries[_key(key)] end ---Set cache entry. ---@param key string[]|string ---@param val any function Cache:set(key, val) key = _key(key) self.keys[key] = true self.entries[key] = val end ---Delete cache entry. ---@param key string[]|string function Cache:del(key) key = _key(key) self.keys[key] = nil self.entries[key] = nil end ---Return this cache has the key entry or not. ---@param key string[]|string ---@return boolean function Cache:has(key) key = _key(key) return not not self.keys[key] end ---Ensure cache entry. ---@generic T ---@param key string[]|string ---@param callback function(): T ---@return T function Cache:ensure(key, callback) if not self:has(key) then self:set(key, callback()) end return self:get(key) end return Cache ================================================ FILE: lua/translate/kit/Cache.spec.lua ================================================ local Cache = require("___plugin_name___.kit.Cache") describe("kit.Cache", function() it("should works {get,set,has,del}", function() local cache = Cache.new() assert.equal(cache:get("unknown"), nil) assert.equal(cache:has("unknown"), false) cache:set("known", nil) assert.equal(cache:get("known"), nil) assert.equal(cache:has("known"), true) cache:del("known") assert.equal(cache:get("known"), nil) assert.equal(cache:has("known"), false) end) it("should work ensure", function() local ensure = setmetatable({ count = 0, }, { __call = function(self) self.count = self.count + 1 end, }) local cache = Cache.new() -- Ensure the value. assert.equal(cache:ensure("key", ensure), nil) assert.equal(cache:has("key"), true) assert.equal(ensure.count, 1) -- Doesn't call when the value was ensured. assert.equal(cache:ensure("key", ensure), nil) assert.equal(ensure.count, 1) -- Call after delete. cache:del("key") assert.equal(cache:ensure("key", ensure), nil) assert.equal(ensure.count, 2) end) end) ================================================ FILE: lua/translate/kit/Config.lua ================================================ local kit = require("___plugin_name___.kit") local Cache = require("___plugin_name___.kit.Cache") ---@class ___plugin_name___.kit.Config.Schema # kit.macro.remove ---@alias ___plugin_name___.kit.Config.SchemaInternal ___plugin_name___.kit.Config.Schema|{ revision: integer } ---@class ___plugin_name___.kit.Config ---@field private _cache ___plugin_name___.kit.Cache ---@field private _default ___plugin_name___.kit.Config.SchemaInternal ---@field private _global ___plugin_name___.kit.Config.SchemaInternal ---@field private _filetype table ---@field private _buffer table local Config = {} ---Create new config instance. ---@param default? ___plugin_name___.kit.Config.Schema function Config.new(default) local self = setmetatable({}, { __index = Config }) self._cache = Cache.new() self._default = default or {} self._global = {} self._filetype = {} self._buffer = {} return self end ---Set default configuration. ---@param default ___plugin_name___.kit.Config.Schema function Config:default(default) self._default = default end ---Update global config. ---@param config ___plugin_name___.kit.Config.Schema function Config:global(config) local revision = (self._global.revision or 1) + 1 self._global = config or {} self._global.revision = revision end ---Update filetype config. ---@param filetypes string|string[] ---@param config ___plugin_name___.kit.Config.Schema function Config:filetype(filetypes, config) for _, filetype in ipairs(kit.to_array(filetypes)) do local revision = ((self._filetype[filetype] or {}).revision or 1) + 1 self._filetype[filetype] = config or {} self._filetype[filetype].revision = revision end end ---Update filetype config. ---@param bufnr integer ---@param config ___plugin_name___.kit.Config.Schema function Config:buffer(bufnr, config) bufnr = bufnr == 0 and vim.api.nvim_get_current_buf() or bufnr local revision = ((self._buffer[bufnr] or {}).revision or 1) + 1 self._buffer[bufnr] = config or {} self._buffer[bufnr].revision = revision end ---Create setup interface. ---@return fun(config: ___plugin_name___.kit.Config.Schema)|{ filetype: fun(filetypes: string|string[], config: ___plugin_name___.kit.Config.Schema), buffer: fun(bufnr: integer, config: ___plugin_name___.kit.Config.Schema) } function Config:create_setup_interface() return setmetatable({}, { ---@param config ___plugin_name___.kit.Config.Schema __call = function(_, config) self:global(config) end, ---@param filetypes string|string[] ---@param config ___plugin_name___.kit.Config.Schema filetype = function(_, filetypes, config) self:filetype(filetypes, config) end, ---@param bufnr integer ---@param config ___plugin_name___.kit.Config.Schema buffer = function(_, bufnr, config) self:buffer(bufnr, config) end, }) end ---Get current configuration. ---@return ___plugin_name___.kit.Config.Schema function Config:get() local filetype = vim.api.nvim_buf_get_option(0, "filetype") local bufnr = vim.api.nvim_get_current_buf() return self._cache:ensure({ self._global.revision or 0, (self._buffer[bufnr] or {}).revision or 0, (self._filetype[filetype] or {}).revision or 0, }, function() local config = self._default config = kit.merge(self._global, config) config = kit.merge(self._filetype[filetype] or {}, config) config = kit.merge(self._buffer[bufnr] or {}, config) config.revision = nil return config end) end return Config ================================================ FILE: lua/translate/kit/Config.spec.lua ================================================ local Config = require("___plugin_name___.kit.Config") describe("kit.Config", function() before_each(function() vim.cmd([[enew]]) end) it("should {setup,get} global config", function() local config = Config.new() config:global({ key = 1 }) assert.are.same(config:get(), { key = 1 }) end) it("should {setup,get} filetype config", function() local config = Config.new() vim.cmd([[set filetype=lua]]) config:filetype("lua", { key = 1 }) assert.are.same(config:get(), { key = 1 }) vim.cmd([[set filetype=]]) assert.are.same(config:get(), {}) end) it("should {setup,get} buffer config", function() local config = Config.new() config:buffer(0, { key = 1 }) assert.are.same(config:get(), { key = 1 }) vim.cmd([[new]]) assert.are.same(config:get(), {}) end) it("should merge configuration", function() local config = Config.new() local bufnr = vim.api.nvim_get_current_buf() vim.cmd([[set filetype=lua]]) config:global({ global = 1 }) config:filetype("lua", { filetype = 1 }) config:buffer(0, { buffer = 1 }) assert.are.same(config:get(), { global = 1, filetype = 1, buffer = 1 }) vim.cmd([[set filetype=]]) assert.are.same(config:get(), { global = 1, buffer = 1 }) vim.cmd([[new]]) assert.are.same(config:get(), { global = 1 }) vim.cmd(([[%sbuffer]]):format(bufnr)) assert.are.same(config:get(), { global = 1, buffer = 1 }) vim.cmd([[set filetype=lua]]) assert.are.same(config:get(), { global = 1, filetype = 1, buffer = 1 }) end) end) ================================================ FILE: lua/translate/kit/LSP/Position.lua ================================================ local Buffer = require("___plugin_name___.kit.Vim.Buffer") ---@class ___plugin_name___.kit.LSP.Position ---@field public line integer ---@field public character integer local Position = {} ---@alias ___plugin_name___.kit.LSP.Position.Encoding 'utf8'|'utf16'|'utf32' Position.Encoding = {} Position.Encoding.UTF8 = "utf8" Position.Encoding.UTF16 = "utf16" Position.Encoding.UTF32 = "utf32" ---Return the value is position or not. ---@param v any ---@return boolean function Position.is(v) return type(v) == "table" and type(v.line) == "number" and type(v.character) == "number" end ---Create cursor position. ---@param encoding ___plugin_name___.kit.LSP.Position.Encoding ---@return ___plugin_name___.kit.LSP.Position function Position.cursor(encoding) encoding = encoding or Position.Encoding.UTF16 local cursor = vim.api.nvim_win_get_cursor(0) local text = vim.api.nvim_get_current_line() local utf8 = { line = cursor[1] - 1, character = cursor[2] } if encoding == Position.Encoding.UTF8 then return utf8 elseif encoding == Position.Encoding.UTF16 then return Position.to_utf16(text, utf8, Position.Encoding.UTF8) elseif encoding == Position.Encoding.UTF32 then return Position.to_utf32(text, utf8, Position.Encoding.UTF8) end end ---Convert position to utf8 from specified encoding. ---@param expr string|integer ---@param position ___plugin_name___.kit.LSP.Position ---@param from_encoding ___plugin_name___.kit.LSP.Position.Encoding ---@return ___plugin_name___.kit.LSP.Position function Position.to_vim(expr, position, from_encoding) if from_encoding == Position.Encoding.UTF8 then return position end local text = Buffer.at(expr, position.line) if from_encoding == Position.Encoding.UTF16 then return Position.to_utf8(text, position, Position.Encoding.UTF16) elseif from_encoding == Position.Encoding.UTF32 then return Position.to_utf8(text, position, Position.Encoding.UTF32) end end ---Convert position to utf8 from specified encoding. ---@param text string ---@param position ___plugin_name___.kit.LSP.Position ---@param from_encoding? ___plugin_name___.kit.LSP.Position.Encoding ---@return ___plugin_name___.kit.LSP.Position function Position.to_utf8(text, position, from_encoding) from_encoding = from_encoding or Position.Encoding.UTF16 if from_encoding == Position.Encoding.UTF8 then return position end local ok, byteindex = pcall(function() return vim.str_byteindex(text, position.character, from_encoding == Position.Encoding.UTF16) end) if not ok then return position end return { line = position.line, character = byteindex } end ---Convert position to utf16 from specified encoding. ---@param text string ---@param position ___plugin_name___.kit.LSP.Position ---@param from_encoding? ___plugin_name___.kit.LSP.Position.Encoding ---@return ___plugin_name___.kit.LSP.Position function Position.to_utf16(text, position, from_encoding) local utf8 = Position.to_utf8(text, position, from_encoding) for index = utf8.character, 0, -1 do local ok, utf16index = pcall(function() return select(2, vim.str_utfindex(text, index)) end) if ok then return { line = utf8.line, character = utf16index } end end return position end ---Convert position to utf32 from specified encoding. ---@param text string ---@param position ___plugin_name___.kit.LSP.Position ---@param from_encoding? ___plugin_name___.kit.LSP.Position.Encoding ---@return ___plugin_name___.kit.LSP.Position function Position.to_utf32(text, position, from_encoding) local utf8 = Position.to_utf8(text, position, from_encoding) for index = utf8.character, 0, -1 do local ok, utf32index = pcall(function() return select(1, vim.str_utfindex(text, index)) end) if ok then return { line = utf8.line, character = utf32index } end end return position end return Position ================================================ FILE: lua/translate/kit/LSP/Position.spec.lua ================================================ local Position = require("___plugin_name___.kit.LSP.Position") describe("kit.LSP.Position", function() local text = "🗿🗿🗿" local utf8 = #text local utf16 = select(2, vim.str_utfindex(text, utf8)) local utf32 = select(1, vim.str_utfindex(text, utf8)) before_each(function() vim.cmd(([[ enew! set noswapfile call setline(1, ['%s']) ]]):format(text)) end) for _, to in ipairs({ { method = "to_utf8", encoding = Position.Encoding.UTF8, character = utf8, }, { method = "to_utf16", encoding = Position.Encoding.UTF16, character = utf16, }, { method = "to_utf32", encoding = Position.Encoding.UTF32, character = utf32, }, }) do for _, from in ipairs({ { character = utf8, encoding = Position.Encoding.UTF8 }, { character = utf16, encoding = Position.Encoding.UTF16 }, { character = utf32, encoding = Position.Encoding.UTF32 }, }) do it(("should convert %s <- %s"):format(to.encoding, from.encoding), function() local converted = Position[to.method](text, { line = 1, character = from.character }, from.encoding) assert.are.same(to.character, converted.character) end) end end end) ================================================ FILE: lua/translate/kit/LSP/Range.lua ================================================ local Position = require("___plugin_name___.kit.LSP.Position") ---@class ___plugin_name___.kit.LSP.Range ---@field public start ___plugin_name___.kit.LSP.Position ---@field public ['end'] ___plugin_name___.kit.LSP.Position local Range = {} ---Return the value is range or not. ---@param v any ---@return boolean function Range.is(range) return type(range) == "table" and Position.is(range.start) and Position.is(range["end"]) end ---Return the range is empty or not. ---@param range ___plugin_name___.kit.LSP.Range ---@return boolean function Range.empty(range) return range.start.line == range["end"].line and range.start.character == range["end"].character end ---Convert range to utf8 from specified encoding. ---@param expr string|integer ---@param range ___plugin_name___.kit.LSP.Range ---@param from_encoding ___plugin_name___.kit.LSP.Position.Encoding ---@return ___plugin_name___.kit.LSP.Range function Range.to_vim(expr, range, from_encoding) return { start = Position.to_vim(expr, range.start, from_encoding), ["end"] = Position.to_vim(expr, range["end"], from_encoding), } end return Range ================================================ FILE: lua/translate/kit/LSP/Range.spec.lua ================================================ local Range = require("___plugin_name___.kit.LSP.Range") describe("kit.LSP.Range", function() it("should return the range is empty or not", function() local position1 = { line = 0, character = 0 } local position2 = { line = 0, character = 1 } assert.are.equal(Range.empty({ start = position1, ["end"] = position1 }), true) assert.are.equal(Range.empty({ start = position1, ["end"] = position2 }), false) end) end) ================================================ FILE: lua/translate/kit/Lua/TreeSitter.lua ================================================ local TreeSitter = {} ---@alias ___plugin_name___.kit.Lua.TreeSitter.VisitStatus 'stop'|'skip' TreeSitter.VisitStatus = {} TreeSitter.VisitStatus.Stop = "stop" TreeSitter.VisitStatus.Skip = "skip" ---Get the leaf node at the specified position. ---@param row integer # 0-based ---@param col integer # 0-based ---@return userdata? function TreeSitter.get_node_at(row, col) local parser = TreeSitter.get_parser() if not parser then return end for _, tree in ipairs(parser:trees()) do local node = tree:root():descendant_for_range(row, col, row, col) if node then local leaf = TreeSitter.get_first_leaf(node) if leaf then return leaf end end end end ---Get first leaf node within the specified node. ---@param node userdata ---@return userdata? function TreeSitter.get_first_leaf(node) if node:child_count() > 0 then return TreeSitter.get_first_leaf(node:child(0)) end return node end ---Get last leaf node within the specified node. ---@param node userdata ---@return userdata? function TreeSitter.get_last_leaf(node) if node:child_count() > 0 then return TreeSitter.get_last_leaf(node:child(node:child_count() - 1)) end return node end ---Get next leaf node. ---@param node userdata ---@return userdata? function TreeSitter.get_next_leaf(node) local function next(node_) local next_sibling = node_:next_sibling() if next_sibling then return TreeSitter.get_first_leaf(next_sibling) else local parent = node_:parent() while parent do next_sibling = parent:next_sibling() if next_sibling then return TreeSitter.get_first_leaf(next_sibling) end parent = parent:parent() end end end return next(TreeSitter.get_first_leaf(node)) end ---Get prev leaf node. ---@param node userdata ---@return userdata function TreeSitter.get_prev_leaf(node) local function prev(node_) local prev_sibling = node_:prev_sibling() if prev_sibling then return TreeSitter.get_last_leaf(prev_sibling) else local parent = node_:parent() while parent do prev_sibling = parent:prev_sibling() if prev_sibling then return TreeSitter.get_last_leaf(prev_sibling) end parent = parent:parent() end end end return prev(TreeSitter.get_last_leaf(node)) end ---Return the node contained the position or not. ---@param node userdata ---@param row integer # 0-based ---@param col integer # 0-based ---@param option { s: boolean, e: boolean } ---@return boolean function TreeSitter.within(node, row, col, option) option = option or {} option.s = option.s ~= nil and option.s or true option.e = option.e ~= nil and option.e or false local s_row, s_col, e_row, e_col = node:range() local s_in = s_row < row or (s_row == row and (option.s and (s_col <= col) or (s_col < col))) local e_in = row < e_row or (row == e_row and (option.e and (col <= e_col) or (col < e_col))) return s_in and e_in end ---Extract nodes that matched the specified mapping. ---@param scope userdata ---@param mapping table ---@return userdata[] function TreeSitter.extract(scope, mapping) local nodes = {} for node_type, next_mapping in pairs(mapping) do if node_type == scope:type() then if type(next_mapping) == "table" then for c in scope:iter_children() do for _, node in ipairs(TreeSitter.extract(c, next_mapping)) do table.insert(nodes, node) end end elseif next_mapping == true then table.insert(nodes, scope) end end end return nodes end ---Return the node is matched the specified mapping. ---@param node userdata ---@param mapping table ---@return userdata? function TreeSitter.matches(node, mapping) local parent = node while parent do if vim.tbl_contains(TreeSitter.extract(parent, mapping), node) then return parent end parent = parent:parent() end end ---Search next specific node. ---@param node userdata ---@param predicate fun(node: userdata): boolean ---@return userdata? function TreeSitter.search_next(node, predicate) local current = node while current do -- down search. local matched = nil TreeSitter.visit(current, function(node_) if node ~= node_ and predicate(node_) then matched = node_ return TreeSitter.VisitStatus.Stop end end) if matched then return matched end -- up search. while current do local next_sibling = current:next_sibling() if next_sibling then current = next_sibling break end current = current:parent() end end end ---Search specific parent node. ---@param node userdata ---@param predicate fun(node: userdata): boolean ---@return userdata? function TreeSitter.search_parent(node, predicate) local parent = node:parent() while parent do if predicate(parent) then return parent end parent = parent:parent() end end ---Get all parents. ---@param node userdata ---@return userdata[] function TreeSitter.parents(node) local parents = {} while node do table.insert(parents, 1, node) node = node:parent() end return parents end ---Visit all nodes. ---@param scope userdata ---@param predicate fun(node: userdata, ctx: { depth: integer }): boolean ---@param option? { reversed: boolean } function TreeSitter.visit(scope, predicate, option) option = option or { reversed = false } local function visit(node, ctx) if not node then return true end local status = predicate(node, ctx) if status == TreeSitter.VisitStatus.Stop then return status -- stop visitting. elseif status ~= TreeSitter.VisitStatus.Skip then local init, last, step if option.reversed then init, last, step = node:child_count() - 1, 0, -1 else init, last, step = 0, node:child_count() - 1, 1 end for i = init, last, step do if visit(node:child(i), { depth = ctx.depth + 1 }) == TreeSitter.VisitStatus.Stop then return TreeSitter.VisitStatus.Stop end end end end return visit(scope, { depth = 1 }) end ---Return the node is matched the specified capture. ---@param query userdata ---@param node userdata ---@return boolean function TreeSitter.is_capture(query, node, capture) for id, match in query:iter_captures(node:parent()) do if match:id() == node:id() and query.captures[id] == capture then return true end end return false end ---Get node text. ---@param node userdata ---@return string[] function TreeSitter.get_node_text(node) local ok, text = pcall(function() local args = { 0, node:range() } table.insert(args, {}) return vim.api.nvim_buf_get_text(unpack(args)) end) if not ok then return { "" } end return text end ---Get parser. ---@return table function TreeSitter.get_parser() return vim.treesitter.get_parser(0, vim.api.nvim_buf_get_option(0, "filetype")) end ---Dump node or node-table. ---@param node userdata|userdata[] function TreeSitter.dump(node) if not node then return print(node) end if type(node) == "table" then if #node == 0 then return print("empty table") end for _, v in ipairs(node) do TreeSitter.dump(v) end return end local message = node:type() local current = node:parent() while current do message = current:type() .. " ~ " .. message current = current:parent() if not current then break end end print(message) end return TreeSitter ================================================ FILE: lua/translate/kit/Lua/TreeSitter.spec.lua ================================================ ---@diagnostic disable: need-check-nil, param-type-mismatch local helper = require("kit.helper") local TreeSitter = require("___plugin_name___.kit.Lua.TreeSitter") describe("kit.Lua.TreeSitter", function() before_each(function() vim.cmd([[ enew! syntax off set filetype=lua call setline(1, [ \ 'function A()', \ ' return 1', \ 'end', \ 'if "then" then', \ ' print(a())', \ 'elseif "else if" then', \ ' print(a())', \ 'elseif "else if" then', \ ' if "then" then', \ ' return 1', \ ' end', \ 'else', \ ' print(a())', \ 'end', \ ]) ]]) end) describe("get_next_leaf & get_prev_leaf", function() it("should return all leaves", function() local current, lines = nil, vim.api.nvim_buf_get_lines(0, 0, -1, false) current = TreeSitter.get_node_at(0, 0) local next_leaves = {} while current do table.insert(next_leaves, TreeSitter.get_node_text(current)) current = TreeSitter.get_next_leaf(current) end current = TreeSitter.get_node_at(#lines - 1, #lines[#lines] - 1) local prev_leaves = {} while current do table.insert(prev_leaves, 1, TreeSitter.get_node_text(current)) current = TreeSitter.get_prev_leaf(current) end assert.are.same(next_leaves, prev_leaves) end) end) describe("get_captures", function() it("should return all captured name", function() vim.treesitter.set_query( "lua", "pairs", [[ [ (function_declaration [ ("function" @pair) ("end" @pair) ]) ] @pair_context ]] ) local node = TreeSitter.get_node_at(0, 0) assert.is_true(TreeSitter.is_capture(vim.treesitter.get_query("lua", "pairs"), node, "pair")) end) end) end) ================================================ FILE: lua/translate/kit/Lua/init.lua ================================================ local Lua = {} ---Create gabage collection detector. ---@param callback fun(...: any): any ---@return userdata function Lua.gc(callback) local gc = newproxy(true) getmetatable(gc).__gc = callback return gc end return Lua ================================================ FILE: lua/translate/kit/Lua/init.spec.lua ================================================ local Lua = require("___plugin_name___.kit.Lua") describe("kit.Lua", function() it("should detect gc timing.", function() local called = false local object = { marker = Lua.gc(function() called = true end), } object = nil collectgarbage("collect") assert.are.equals(object, nil) assert.are.equals(called, true) end) end) ================================================ FILE: lua/translate/kit/Vim/Buffer.lua ================================================ local kit = require("___plugin_name___.kit") local Highlight = require("___plugin_name___.kit.Vim.Highlight") local Buffer = {} ---Ensure buffer number. ---NOTE: This function only supports '%' as special symbols. ---NOTE: This function uses `vim.fn.bufload`. It can cause side-effect. ---@param expr string|number ---@return number function Buffer.ensure(expr) if type(expr) == "number" then if not vim.api.nvim_buf_is_valid(expr) then error(string.format([=[[kit.Vim.Buffer] expr=`%s` is not a valid]=], expr)) end else if expr == "%" then expr = vim.api.nvim_get_current_buf() end if vim.fn.bufexists(expr) == 0 then expr = vim.fn.bufadd(expr) vim.api.nvim_buf_set_option(expr, "buflisted", true) else expr = vim.fn.bufnr(expr) end end if not vim.api.nvim_buf_is_loaded(expr) then vim.fn.bufload(expr) end return expr end ---Get buffer line. ---@param expr string|number ---@param line number ---@return string function Buffer.at(expr, line) return vim.api.nvim_buf_get_lines(Buffer.ensure(expr), line, line + 1, false)[1] or "" end ---Open buffer. ---@param cmd table # The `new` command argument. See :help nvim_parse_cmd()` ---@param range? ___plugin_name___.kit.Vim.Range function Buffer.open(cmd, range) vim.cmd.new(cmd) local Range = require("___plugin_name___.kit.LSP.Range") if Range.is(range) and not Range.empty(range) then vim.api.nvim_win_set_cursor(0, { range.start.line + 1, range.start.character }) Highlight.blink(range) end end return Buffer ================================================ FILE: lua/translate/kit/Vim/Buffer.spec.lua ================================================ local Buffer = require("___plugin_name___.kit.Vim.Buffer") describe("kit.Vim.Buffer", function() before_each(function() vim.cmd([[ enew! set noswapfile ]]) end) it("should ensure bufnr via didn't loaded filename", function() local buf = Buffer.ensure(vim.api.nvim_get_runtime_file("syntax/markdown.vim", true)[1]) assert.are.equal(vim.api.nvim_buf_get_option(buf, "buflisted"), true) assert.are.equal(vim.api.nvim_buf_is_valid(buf), true) assert.are.equal(vim.api.nvim_buf_is_loaded(buf), true) assert.are.equal(#vim.api.nvim_buf_get_lines(buf, 0, -1, true), 169) end) it("should ensure bufnr via pseudo filename", function() local buf = Buffer.ensure("this-file-is-not-exists") assert.are.equal(vim.api.nvim_buf_get_option(buf, "buflisted"), true) assert.are.equal(vim.api.nvim_buf_is_valid(buf), true) assert.are.equal(vim.api.nvim_buf_is_loaded(buf), true) assert.are.equal(#vim.api.nvim_buf_get_lines(buf, 0, -1, true), 1) end) it("should ensure bufnr via existing buffer", function() local org = vim.api.nvim_get_current_buf() local buf = Buffer.ensure(org) assert.are.equal(org, buf) assert.are.equal(vim.api.nvim_buf_get_option(buf, "buflisted"), true) assert.are.equal(vim.api.nvim_buf_is_valid(buf), true) assert.are.equal(vim.api.nvim_buf_is_loaded(buf), true) assert.are.equal(#vim.api.nvim_buf_get_lines(buf, 0, -1, true), 1) end) end) ================================================ FILE: lua/translate/kit/Vim/Highlight.lua ================================================ local kit = require("___plugin_name___.kit") local Async = require("___plugin_name___.kit.Async") local AsyncTask = require("___plugin_name___.kit.Async.AsyncTask") local Highlight = {} Highlight.namespace = vim.api.nvim_create_namespace("___plugin_name___.kit.Vim.Highlight") ---Blink specified range. ---@param range ___plugin_name___.kit.LSP.Range ---@param option? { delay: integer, count: integer } ---@return ___plugin_name___.kit.Async.AsyncTask function Highlight.blink(range, option) option = kit.merge(option or {}, { delay = 150, count = 2, }) local function timeout(timeout) return AsyncTask.new(function(resolve) vim.defer_fn(vim.schedule_wrap(resolve), timeout) end) end return Async.run(function() Async.await(timeout(option.delay * 1.2)) for i = 1, option.count do vim.highlight.range( 0, Highlight.namespace, "IncSearch", { range.start.line, range.start.character }, { range["end"].line, range["end"].character }, {} ) Async.await(timeout(option.delay * 0.8)) vim.api.nvim_buf_clear_namespace(0, Highlight.namespace, 0, -1) Async.await(timeout(option.delay)) end end) end return Highlight ================================================ FILE: lua/translate/kit/Vim/Highlight.spec.lua ================================================ local Highlight = require("___plugin_name___.kit.Vim.Highlight") describe("kit.Vim.Highlight", function() it("should not throw error", function() Highlight.blink({ start = { line = 0, character = 0 }, ["end"] = { line = 0, character = 0 }, }):sync() end) end) ================================================ FILE: lua/translate/kit/Vim/Keymap.lua ================================================ local AsyncTask = require("___plugin_name___.kit.Async.AsyncTask") local Keymap = {} Keymap._callbacks = {} ---Replace termcodes. ---@param keys string ---@return string function Keymap.termcodes(keys) return vim.api.nvim_replace_termcodes(keys, true, true, true) end ---Send keys. ---@param keys string ---@param mode string function Keymap.send(keys, mode) local callback = Keymap.termcodes('lua require("___plugin_name___.kit.Vim.Keymap")._resolve()') return AsyncTask.new(function(resolve) table.insert(Keymap._callbacks, resolve) if string.match(mode, "i") then vim.api.nvim_feedkeys(callback, "in", true) vim.api.nvim_feedkeys(keys, mode, true) else vim.api.nvim_feedkeys(keys, mode, true) vim.api.nvim_feedkeys(callback, "n", true) end end) end ---Test spec helper. ---@param spec fun(): any function Keymap.spec(spec) local task = AsyncTask.resolve():next(spec) vim.api.nvim_feedkeys("", "x", true) task:sync() collectgarbage("collect") end ---Resolve running keys. function Keymap._resolve() table.remove(Keymap._callbacks, 1)() end return Keymap ================================================ FILE: lua/translate/kit/Vim/Keymap.spec.lua ================================================ local Async = require("___plugin_name___.kit.Async") local Keymap = require("___plugin_name___.kit.Vim.Keymap") local async = Async.async local await = Async.await describe("kit.Vim.Keymap", function() it("should insert keysequence with async-await", function() vim.keymap.set( "i", "(kit.Vim.Keymap.send)", async(function() await(Keymap.send("foo", "in")) await(Keymap.send("bar", "in")) await(Keymap.send("baz", "in")) end) ) Keymap.spec(async(function() await(Keymap.send(Keymap.termcodes("i{(kit.Vim.Keymap.send)}"), "i")) end)) --NOTE: The `i` flag works only first time. assert.are.equals(vim.api.nvim_get_current_line(), "{foo}barbaz") end) end) ================================================ FILE: lua/translate/kit/Vim/Syntax.lua ================================================ local kit = require("___plugin_name___.kit") local Syntax = {} ---Get all syntax groups for specified position. ---NOTE: This function accepts 0-origin cursor position. ---@param cursor number[] ---@return string[] function Syntax.get_syntax_groups(cursor) return kit.concat(Syntax.get_vim_syntax_groups(cursor), Syntax.get_treesitter_syntax_groups(cursor)) end ---Get vim's syntax groups for specified position. ---NOTE: This function accepts 0-origin cursor position. ---@param cursor number[] ---@return string[] function Syntax.get_vim_syntax_groups(cursor) local groups = {} for _, syntax_id in ipairs(vim.fn.synstack(cursor[1] + 1, cursor[2] + 1)) do table.insert(groups, vim.fn.synIDattr(vim.fn.synIDtrans(syntax_id), "name")) end return groups end ---Get tree-sitter's syntax groups for specified position. ---NOTE: This function accepts 0-origin cursor position. ---@param cursor number[] ---@return string[] function Syntax.get_treesitter_syntax_groups(cursor) local groups = {} for _, capture in ipairs(vim.treesitter.get_captures_at_pos(0, cursor[1], cursor[2])) do table.insert(groups, ("@%s"):format(capture.capture)) end return groups end return Syntax ================================================ FILE: lua/translate/kit/Vim/Syntax.spec.lua ================================================ local helper = require("kit.helper") local Syntax = require("___plugin_name___.kit.Vim.Syntax") describe("kit.Vim.Syntax", function() before_each(function() vim.cmd([[ enew! set filetype=vim call setline(1, ['let var = 1']) ]]) end) it("should return vim syntax group", function() vim.cmd([[ syntax on ]]) assert.are.same(Syntax.get_syntax_groups({ 0, 3 }), {}) assert.are.same(Syntax.get_syntax_groups({ 0, 4 }), { "Identifier" }) assert.are.same(Syntax.get_syntax_groups({ 0, 6 }), { "Identifier" }) assert.are.same(Syntax.get_syntax_groups({ 0, 7 }), {}) end) it("should return treesitter syntax group", function() helper.ensure_treesitter_parser("vim") vim.cmd([[ syntax off ]]) assert.are.same(Syntax.get_syntax_groups({ 0, 3 }), {}) assert.are.same(Syntax.get_syntax_groups({ 0, 4 }), { "@variable" }) assert.are.same(Syntax.get_syntax_groups({ 0, 6 }), { "@variable" }) assert.are.same(Syntax.get_syntax_groups({ 0, 7 }), {}) end) end) ================================================ FILE: lua/translate/kit/init.lua ================================================ --[[ MIT License Copyright (c) 2022 hrsh7th Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ]] -- local kit = {} ---Create unique id. ---@return integer kit.uuid = setmetatable({ uuid = 0, }, { __call = function(self) self.uuid = self.uuid + 1 return self.uuid end, }) -- https://neovim.io/doc/user/deprecated.html#vim.tbl_islist() local islist = vim.islist or vim.tbl_islist ---Merge two tables. ---@generic T ---NOTE: This doesn't merge array-like table. ---@param tbl1 T ---@param tbl2 T ---@return T function kit.merge(tbl1, tbl2) local is_dict1 = type(tbl1) == "table" and (not islist(tbl1) or vim.tbl_isempty(tbl1)) local is_dict2 = type(tbl2) == "table" and (not islist(tbl2) or vim.tbl_isempty(tbl2)) if is_dict1 and is_dict2 then local new_tbl = {} for k, v in pairs(tbl2) do if tbl1[k] ~= vim.NIL then new_tbl[k] = kit.merge(tbl1[k], v) end end for k, v in pairs(tbl1) do if tbl2[k] == nil then new_tbl[k] = v ~= vim.NIL and v or nil end end return new_tbl elseif is_dict1 and not is_dict2 then return kit.merge(tbl1, {}) elseif not is_dict1 and is_dict2 then return kit.merge(tbl2, {}) end if tbl1 == vim.NIL then return nil elseif tbl1 == nil then return tbl2 else return tbl1 end end ---Concatenate two tables. ---NOTE: This doesn't concatenate dict-like table. ---@param tbl1 table ---@param tbl2 table function kit.concat(tbl1, tbl2) local new_tbl = {} for _, item in ipairs(tbl1) do table.insert(new_tbl, item) end for _, item in ipairs(tbl2) do table.insert(new_tbl, item) end return new_tbl end ---The value to array. ---@param value any ---@return table function kit.to_array(value) if type(value) == "table" then if islist(value) or vim.tbl_isempty(value) then return value end end return { value } end ---Check the value is array. ---@param value any ---@return boolean function kit.is_array(value) return type(value) == "table" and (islist(value) or vim.tbl_isempty(value)) end ---Reverse the array. ---@param array table ---@return table function kit.reverse(array) if not kit.is_array(array) then error("[kit] specified value is not an array.") end local new_array = {} for i = #array, 1, -1 do table.insert(new_array, array[i]) end return new_array end ---Map array values. ---@generic T ---@param array T[] ---@parma func fun(item: T, index: number): V ---@reutrn T[] function kit.map(array, func) local new_array = {} for i, item in ipairs(array) do table.insert(new_array, func(item, i)) end return new_array end return kit ================================================ FILE: lua/translate/kit/init.spec.lua ================================================ local kit = require("___plugin_name___.kit") describe("kit", function() describe(".merge", function() it("should merge two dict", function() assert.are.same( kit.merge({ a = true, b = { c = vim.NIL, }, d = { e = 3, }, }, { a = false, b = { c = true, }, d = { f = { g = vim.NIL, }, }, }), { a = true, b = {}, d = { e = 3, f = {}, }, } ) end) end) describe(".concat", function() it("should concat two list", function() assert.are.same(kit.concat({ 1, 2, 3 }, { 4, 5, 6 }), { 1, 2, 3, 4, 5, 6 }) end) end) describe(".to_array", function() it("should convert value to array", function() assert.are.same(kit.to_array(1), { 1 }) assert.are.same(kit.to_array({ 1, 2, 3 }), { 1, 2, 3 }) assert.are.same(kit.to_array({}), {}) assert.are.same(kit.to_array({ a = 1 }), { { a = 1 } }) end) end) describe(".is_array", function() it("should check array or not", function() assert.are.equal(kit.is_array({}), true) assert.are.equal(kit.is_array({ 1 }), true) assert.are.equal(kit.is_array({ a = 1 }), false) assert.are.equal(kit.is_array(1), false) end) end) describe(".reverse", function() it("should reverse the array", function() assert.are.same(kit.reverse({ 1, 2, 3 }), { 3, 2, 1 }) end) end) describe(".map", function() it("should map array values", function() local array = kit.map({ "1", "2", "3" }, function(v) return tonumber(v, 10) end) assert.are.same(array, { 1, 2, 3 }) end) end) end) ================================================ FILE: lua/translate/preset/command/deepl.lua ================================================ local M = {} local json_encode = vim.json and vim.json.encode or vim.fn.json_encode ---@param url string ---@param lines string[] ---@param command_args table ---@return string ---@return string[] function M._cmd(url, lines, command_args) if not vim.g.deepl_api_auth_key then error("[translate.nvim] Set your DeepL API authorization key to g:deepl_api_auth_key.") end local cmd = "curl" local args = { "-X", "POST", "-s", url, "--header", "Content-Type: application/json", "--header", "Authorization: DeepL-Auth-Key " .. vim.g.deepl_api_auth_key, "--data", json_encode({ text = lines, target_lang = command_args.target, source_lang = command_args.source, }) } return cmd, args end function M.complete_list(is_target) -- See local list = { "BG", "CS", "DA", "DE", "EL", "EN", "ES", "ET", "FI", "FR", "HU", "IT", "JA", "LT", "LV", "NL", "PL", "PT", "RO", "RU", "SK", "SL", "SV", "ZH", } if is_target then local append = { "EN-GB", "EN-US", "PT-PT", "PT-BR", } list = vim.list_extend(list, append) end return list end return M ================================================ FILE: lua/translate/preset/command/deepl_free.lua ================================================ local deepl = require("translate.preset.command.deepl") local M = {} ---@param lines string[] ---@param command_args table ---@return string cmd ---@return string[] args function M.cmd(lines, command_args) local url = "https://api-free.deepl.com/v2/translate" local cmd, args = deepl._cmd(url, lines, command_args) local options = require("translate.config").get("preset").command.deepl_free if #options.args > 0 then args = vim.list_extend(args, options.args) end return cmd, args end M.complete_list = deepl.complete_list return M ================================================ FILE: lua/translate/preset/command/deepl_pro.lua ================================================ local deepl = require("translate.preset.command.deepl") local M = {} ---@param lines string[] ---@param command_args table ---@return string cmd ---@return string[] args function M.cmd(lines, command_args) local url = "https://api.deepl.com/v2/translate" local cmd, args = deepl._cmd(url, lines, command_args) local options = require("translate.config").get("preset").command.deepl_pro if #options.args > 0 then args = vim.list_extend(args, options.args) end return cmd, args end M.complete_list = deepl.complete_list return M ================================================ FILE: lua/translate/preset/command/google.lua ================================================ local util = require("translate.util.util") local M = {} M.url = "https://script.google.com/macros/s/AKfycbxLRZgWI3UyHvHuYVyH1StiXbzJDHyibO5XpVZm5kMlXFlzaFVtLReR0ZteEkUbecRpPQ/exec" ---@param lines string[] ---@param command_args table ---@return string ---@return string[] function M.cmd(lines, command_args) local data = vim.json.encode({ text = lines, target = command_args.target, source = command_args.source, }) local cmd, args if vim.fn.has("win32") == 1 then cmd = "cmd.exe" local path = util.write_temp_data(data) args = { "/c", table.concat({ "curl", "-sL", M.url, "-d", "@" .. path, }, " "), } else cmd = "curl" args = { "-sL", M.url, "-d", data, } end local options = require("translate.config").get("preset").command.google if #options.args > 0 then args = vim.list_extend(args, options.args) end return cmd, args end function M.complete_list() -- See local list = { "af", "sq", "am", "ar", "hy", "az", "eu", "be", "bn", "bs", "bg", "ca", "ceb", "zh", "zh-CN", "zh-TW", "co", "hr", "cs", "da", "nl", "en", "eo", "et", "fi", "fr", "fy", "gl", "ka", "de", "el", "gu", "ht", "ha", "haw", "he", "iw", "hi", "hmn", "hu", "is", "ig", "id", "ga", "it", "ja", "jv", "kn", "kk", "km", "rw", "ko", "ku", "ky", "lo", "lv", "lt", "lb", "mk", "mg", "ms", "ml", "mt", "mi", "mr", "mn", "my", "ne", "no", "ny", "or", "ps", "fa", "pl", "pt", "pa", "ro", "ru", "sm", "gd", "sr", "st", "sn", "sd", "si", "sk", "sl", "so", "es", "su", "sw", "sv", "tl", "tg", "ta", "tt", "te", "th", "tr", "tk", "uk", "ur", "ug", "uz", "vi", "cy", "xh", "yi", "yo", "zu", } return list end return M ================================================ FILE: lua/translate/preset/command/translate_shell.lua ================================================ local M = {} ---@param lines string[] ---@param command_args table ---@return string ---@return string[] function M.cmd(lines, command_args) local text = table.concat(lines, "\n") local source = command_args.source or "" local target = command_args.target local cmd = "trans" local args = { "-b", "-no-ansi", "-no-autocorrect", } local options = require("translate.config").get("preset").command.translate_shell if #options.args > 0 then args = vim.list_extend(args, options.args) end table.insert(args, source .. ":" .. target) table.insert(args, text) return cmd, args end function M.complete_list() -- See local list = { "af", "afr", "am", "amh", "ar", "ara", "az", "aze", "ba", "bak", "be", "bel", "bg", "bul", "bn", "ben", "bs", "bos", "ca", "cat", "ceb", "ceb", "co", "cos", "cs", "ces", "cy", "cym", "da", "dan", "de", "deu", "el", "ell", "en", "eng", "eo", "epo", "es", "spa", "et", "est", "eu", "eus", "fa", "fas", "fi", "fin", "fj", "fij", "fr", "fra", "fy", "fry", "ga", "gle", "gd", "gla", "gl", "glg", "gu", "guj", "ha", "hau", "haw", "haw", "he", "heb", "hi", "hin", "hmn", "hmn", "hr", "hrv", "ht", "hat", "hu", "hun", "hy", "hye", "id", "ind", "ig", "ibo", "is", "isl", "it", "ita", "ja", "jpn", "jv", "jav", "ka", "kat", "kk", "kaz", "km", "khm", "kn", "kan", "ko", "kor", "ku", "kur", "ky", "kir", "la", "lat", "lb", "ltz", "lo", "lao", "lt", "lit", "lv", "lav", "mg", "mlg", "mhr", "mhr", "mi", "mri", "mk", "mkd", "ml", "mal", "mn", "mon", "mr", "mar", "mrj", "mrj", "ms", "msa", "mt", "mlt", "mww", "mww", "my", "mya", "ne", "nep", "nl", "nld", "no", "nor", "ny", "nya", "or", "ori", "otq", "otq", "pa", "pan", "pap", "pap", "pl", "pol", "ps", "pus", "pt", "por", "ro", "ron", "ru", "rus", "rw", "kin", "sd", "snd", "si", "sin", "sk", "slk", "sl", "slv", "sm", "smo", "sn", "sna", "so", "som", "sq", "sqi", "sr-Cyrl", "srp", "sr-Latn", "srp", "st", "sot", "su", "sun", "sv", "swe", "sw", "swa", "ta", "tam", "te", "tel", "tg", "tgk", "th", "tha", "tk", "tuk", "tl", "tgl", "tlh", "tlh", "tlh-Qaak", "tlh", "to", "ton", "tr", "tur", "tt", "tat", "ty", "tah", "udm", "udm", "ug", "uig", "uk", "ukr", "ur", "urd", "uz", "uzb", "vi", "vie", "xh", "xho", "yi", "yid", "yo", "yor", "yua", "yua", "yue", "yue", "zh-CN", "zho", "zh-TW", "zho", "zu", "zul", } return list end return M ================================================ FILE: lua/translate/preset/output/floating.lua ================================================ local api = vim.api local util = require("translate.util.util") local M = { window = {}, } function M.cmd(lines, _) if type(lines) == "string" then lines = { lines } end M.window.close() local options = require("translate.config").get("preset").output.floating local buf = api.nvim_create_buf(false, true) api.nvim_buf_set_lines(buf, 0, -1, true, lines) api.nvim_set_option_value("filetype", options.filetype, { buf = buf }) local width = util.max_width_in_string_list(lines) local height = #lines local win = api.nvim_open_win(buf, false, { relative = options.relative, style = options.style, width = width, height = height, row = options.row, col = options.col, border = options.border, zindex = options.zindex, }) M.window._current = { win = win, buf = buf } api.nvim_create_autocmd("CursorMoved", { callback = M.window.close, once = true, }) end function M.window.close() if M.window._current then api.nvim_win_close(M.window._current.win, false) api.nvim_buf_delete(M.window._current.buf, {}) M.window._current = nil end end return M ================================================ FILE: lua/translate/preset/output/insert.lua ================================================ local api = vim.api local M = {} function M.cmd(lines, pos) if type(lines) == "string" then lines = { lines } end local lines_origin = pos._lines for i, line in ipairs(lines) do local p = pos[i] local indent = string.rep(" ", #lines_origin[i]:sub(1, p.col[1] - 1)) lines[i] = indent .. line end local options = require("translate.config").get("preset").output.insert local row if options.base == "top" then row = pos[1].row else -- "bottom" row = pos[#pos].row end row = row + options.off api.nvim_buf_set_lines(0, row, row, false, lines) end return M ================================================ FILE: lua/translate/preset/output/register.lua ================================================ local fn = vim.fn local M = {} ---Set the register ---@param lines string[] function M.cmd(lines, _) local newline local ff = vim.o.fileformat if ff == "unix" then newline = "\n" elseif ff == "dos" then newline = "\r\n" else newline = "\r" end local text = table.concat(lines, newline) local options = require("translate.config").get("preset").output.register local name = options.name fn.setreg(name, text) end return M ================================================ FILE: lua/translate/preset/output/replace.lua ================================================ local api = vim.api local M = {} function M.cmd(lines, pos) if type(lines) == "string" then lines = { lines } end local lines_origin = pos._lines for i, p in ipairs(pos) do local pre = lines_origin[i]:sub(1, p.col[1] - 1) local suf = lines_origin[i]:sub(p.col[2] + 1) lines[i] = pre .. (lines[i] or "") .. suf end api.nvim_buf_set_lines(0, pos[1].row - 1, pos[#pos].row, true, lines) end return M ================================================ FILE: lua/translate/preset/output/split.lua ================================================ local fn = vim.fn local api = vim.api local M = {} function M.cmd(lines, pos) if type(lines) == "string" then lines = { lines } end local lines_origin = pos._lines -- Remain indentation for i, line in ipairs(lines) do local p = pos[i] local indent = string.rep(" ", #lines_origin[i]:sub(1, p.col[1] - 1)) lines[i] = indent .. line end local option = require("translate.config").get("preset").output.split local size = M._get_size(#lines, option) local function split_win() local cmd = option.position == "bottom" and "botright" or "topleft" cmd = cmd .. " " .. size .. "new" vim.cmd(cmd) end local current_win_id = fn.win_getid() if fn.bufexists(option.name) == 1 then local bufnr = fn.bufnr(option.name) local winid = fn.win_findbuf(bufnr) -- Buffer is present, but window is closed if vim.tbl_isempty(winid) then split_win() vim.cmd("e " .. option.name) else fn.win_gotoid(winid[1]) end else split_win() vim.cmd("e " .. option.name) api.nvim_set_option_value("buftype", "nofile", { buf = 0 }) api.nvim_set_option_value("filetype", option.filetype, { buf = 0 }) end if option.append and not M._buf_empty() then api.nvim_buf_set_lines(0, -1, -1, false, lines) else api.nvim_buf_set_lines(0, 0, -1, false, lines) end -- Move cursor to bottom api.nvim_win_set_cursor(0, { fn.line("$"), 0 }) fn.win_gotoid(current_win_id) end function M._buf_empty() if fn.line("$") ~= 1 then return false end local line = fn.getline(1) if line ~= "" then return false end return true end function M._get_size(size, option) local min_size = option.min_size if min_size < 1 then min_size = math.floor(api.nvim_win_get_height(0) * min_size) end local max_size = option.max_size if max_size < 1 then max_size = math.floor(api.nvim_win_get_height(0) * max_size) end if size <= min_size then return min_size end if size >= max_size then return max_size end return size end return M ================================================ FILE: lua/translate/preset/parse_after/deepl.lua ================================================ local M = {} local json_decode = vim.json and vim.json.decode or vim.fn.json_decode ---@param response string #json string ---@return string[] function M.cmd(response) local decoded = json_decode(response) local results = {} for _, r in ipairs(decoded.translations) do table.insert(results, r.text) end return results end return M ================================================ FILE: lua/translate/preset/parse_after/deepl_free.lua ================================================ return require("translate.preset.parse_after.deepl") ================================================ FILE: lua/translate/preset/parse_after/deepl_pro.lua ================================================ return require("translate.preset.parse_after.deepl") ================================================ FILE: lua/translate/preset/parse_after/google.lua ================================================ local M = {} function M.cmd(text, _) return vim.json.decode(text) end return M ================================================ FILE: lua/translate/preset/parse_after/head.lua ================================================ local api = vim.api local util = require("translate.util.util") local M = {} ---Cut the results of translation to fit the original width of the selection. ---The width of the last line cannot be guaranteed because the number of characters changes. ---@param lines string[] ---@param pos table ---@return string[] function M.cmd(lines, pos) local results = {} for i, text in ipairs(lines) do local group = pos._group[i] local widths_origin = {} local sum_width_origin = 0 for _, g in ipairs(group) do local width = api.nvim_strwidth(pos._lines_selected[g]) table.insert(widths_origin, width) sum_width_origin = sum_width_origin + width end local sum_width_result = api.nvim_strwidth(text) local widths = widths_origin if sum_width_origin > sum_width_result then local l = sum_width_origin for j = #widths, 1, -1 do local w = widths[j] l = l - w if l >= sum_width_result then table.remove(widths, j) else widths[j] = sum_width_result - l break end end end if #widths > 1 then local result = util.text_cut(text, widths) local diff = #group - #widths if diff > 0 then for _ = 1, diff do table.insert(result, "") end end results = vim.list_extend(results, result) else table.insert(results, text) end end return results end return M ================================================ FILE: lua/translate/preset/parse_after/no_handle.lua ================================================ local M = {} function M.cmd(lines, _) return lines end return M ================================================ FILE: lua/translate/preset/parse_after/oneline.lua ================================================ local M = {} ---@param lines string[] ---@return string[] function M.cmd(lines, _) lines = { table.concat(lines, "") } return lines end return M ================================================ FILE: lua/translate/preset/parse_after/rate.lua ================================================ local api = vim.api local util = require("translate.util.util") local M = {} ---Cut the results of translation to fit the rate of the original width of the selection. ---@param lines string[] ---@param pos table ---@return string[] function M.cmd(lines, pos) local results = {} for i, text in ipairs(lines) do local group = pos._group[i] local width_origin = {} local sum_width_origin = 0 for _, g in ipairs(group) do local width = api.nvim_strwidth(pos._lines_selected[g]) table.insert(width_origin, width) sum_width_origin = sum_width_origin + width end local sum_width_result = api.nvim_strwidth(text) local width = vim.tbl_map(function(w) return math.floor(w / sum_width_origin * sum_width_result) end, width_origin) if #width > 1 then results = vim.list_extend(results, util.text_cut(text, width)) else table.insert(results, text) end end return results end return M ================================================ FILE: lua/translate/preset/parse_after/translate_shell.lua ================================================ local M = {} ---@param text string ---@return string[] function M.cmd(text, _) local crlf -- Remove the extra CRLF at the end. if vim.endswith(text, "\r\n") then crlf = "\r\n" text = text:sub(1, -3) else crlf = text:sub(-1) text = text:sub(1, -2) end local lines = vim.split(text, crlf) return lines end return M ================================================ FILE: lua/translate/preset/parse_after/window.lua ================================================ local api = vim.api local util = require("translate.util.util") local M = {} ---Cut the text to fit the window width. ---@param lines string[] ---@return string[] function M.cmd(lines, _) local option = require("translate.config").get("preset").parse_after.window local width = option.width if width <= 1 then width = math.floor(api.nvim_win_get_width(0) * option.width) end local results = {} for _, text in ipairs(lines) do results = vim.list_extend(results, util.text_cut(text, width)) end return results end return M ================================================ FILE: lua/translate/preset/parse_before/concat.lua ================================================ local M = {} ---@param lines string[] ---@return string[] function M.cmd(lines) local options = require("translate.config").get("preset").parse_before.concat local sep = options.sep lines = { table.concat(lines, sep) } return lines end return M ================================================ FILE: lua/translate/preset/parse_before/natural.lua ================================================ local util = require("translate.util.util") local M = {} local function inc(tbl, index) if tbl[index] then return index + 1 end return index end ---@param lines string[] ---@param pos positions ---@param cmd_args table ---@return string[] function M.cmd(lines, pos, cmd_args) local option = require("translate.config").get("preset").parse_before.natural local end_regex local start_regex if cmd_args.source then local ends = M.get_end(cmd_args.source, option) if ends then end_regex = vim.regex([[\V\%(]] .. table.concat(ends, [[\|]]) .. [[\)\$]]) end local starts = M.get_start(cmd_args.source, option) if starts then start_regex = vim.regex([[^\V\%(]] .. table.concat(starts, [[\|]]) .. [[\)]]) end end pos._group = {} local results = {} local original_index, result_index = 1, 1 while true do local line = lines[original_index] if not line then break end if line == "" then result_index = inc(results, result_index) util.append_dict_list(results, result_index, line) util.append_dict_list(pos._group, result_index, original_index) if results[result_index] then result_index = result_index + 1 end else if start_regex and start_regex:match_str(line) then result_index = inc(results, result_index) end util.append_dict_list(results, result_index, line) util.append_dict_list(pos._group, result_index, original_index) if end_regex and end_regex:match_str(line) then result_index = inc(results, result_index) end end original_index = original_index + 1 end results = vim.tbl_map(function(r) return table.concat(r, " ") end, results) return results end M.lang_abbr = { en = "english", eng = "english", ja = "japanese", jpn = "japanese", zh = "chinese", zho = "chinese", ["zh-CN"] = "chinese", ["zh-TW"] = "chinese", } -- vim's regex pattern (vary no magic '\V') M.end_marks = { english = { ".", "?", "!", ":", ";", }, japanese = { "。", ".", "?", "?", "!", "!", ":", ";", }, chinese = { "。", "!", "?", ":", }, } -- vim's regex pattern (vary no magic '\V') M.start_marks = { english = { [[\u\U]], }, } function M.get_end(lang, option) lang = lang:lower() lang = option.lang_abbr[lang] or M.lang_abbr[lang] return option.end_marks[lang] or M.end_marks[lang] end function M.get_start(lang, option) lang = lang:lower() lang = option.lang_abbr[lang] or M.lang_abbr[lang] return option.start_marks[lang] or M.start_marks[lang] end return M ================================================ FILE: lua/translate/preset/parse_before/no_handle.lua ================================================ local M = {} ---@param lines string[] ---@return string[] function M.cmd(lines) return lines end return M ================================================ FILE: lua/translate/preset/parse_before/trim.lua ================================================ local M = {} ---@param lines string[] ---@param pos positions ---@return string[] function M.cmd(lines, pos) for i, line in ipairs(lines) do local pre = line:match("^%s*") pos[i].col[1] = pos[i].col[1] + #pre local suf = line:match("%s*$") pos[i].col[2] = pos[i].col[2] - #suf lines[i] = line:sub(#pre + 1, -#suf - 1) end return lines end return M ================================================ FILE: lua/translate/util/comment.lua ================================================ local fn = vim.fn local api = vim.api local context = require("translate.util.context") local util = require("translate.util.util") local M = {} local string_symbols = { python = { { begin = [[''']], last = [[''']] }, { begin = [["""]], last = [["""]] }, }, } function M.get_range() -- example 2. (see below) -- Common comments can be of the following types. -- 1. The comment string repeats at the start of each line (e.g. this line). -- This may be strung together on multiple lines to form a single comment. -- 2. Similar 1., but comments begin in the middle of the line (e.g. the comment four lines above). -- 3. three-piece comment (e.g. c's '/* comment */'). -- -- First, check to see if the current line is 1. by looking at the beginning of the line -- since the only case in which it is necessary to recursively examine is in 1. -- If not 1, then use highlighting or treesitter to take the range of comments. -- We have already established that it is either 2 or 3, so all that remains is to remove the comment sign. local comments = M.get_comments() -- (1, 1) indexed cursor position local cursor = api.nvim_win_get_cursor(0) cursor[2] = cursor[2] + 1 local pos = { _mode = "comment", } if M.is_pattern1(comments, cursor[1], pos) then return pos end -- { row_s, col_s, row_e, col_e } local range = context.ts.get_range("comment", cursor) or context.vim.get_range("Comment", cursor) if range then M.remove_comment_symbol(comments, range, pos) else -- filetype check local ft = vim.bo.filetype if vim.tbl_contains(vim.tbl_keys(string_symbols), ft) then range = context.ts.get_range("string", cursor) or context.vim.get_range("String", cursor) if range then M.remove_string_symbol(string_symbols[ft], range, pos) end else vim.notify("Here is not in comments.") end end return pos end function M.get_comments() -- Ignore 'n' and 'f' because they are complicated and not used often. local comments = {} for comment in vim.gsplit(vim.bo.comments, ",") do local flags, com = comment:match("^(.*):(.*)$") if flags:find("b") then -- Blank required after com com = com .. [[\s]] end if flags:find("s") then -- Start of three-piece comment util.append_dict_list(comments, "s", com) elseif flags:find("m") then -- Middle of three-piece comment util.append_dict_list(comments, "m", com) elseif flags:find("e") then -- End of three-piece comment util.append_dict_list(comments, "e", com) elseif not flags:find("f") then -- When flags have none of the 'f', 's', 'm' or 'e' flags, Vim assumes the comment -- string repeats at the start of each line. The flags field may be empty. util.append_dict_list(comments, "empty", com) end end comments = vim.tbl_map(function(c) return [[\V\%(]] .. table.concat(c, [[\|]]) .. [[\)]] end, comments) return comments end function M.remove_comment_symbol(comments, range, pos) local lines = api.nvim_buf_get_lines(0, range[1] - 1, range[3], true) pos._lines = lines if range[1] == range[3] and M.is_pattern2(comments, range, pos) then return pos end -- If you have made it this far, it should be pattern 3. -- So if it fails inside is_pattern3, it is an error. M.assert_pattern3(comments, range, pos) return pos end ---Check if a line of 'row' is pattern 1, and if so, check if the lines above and below they are also pattern 1. ---Even if the pattern is 1, if the indentation and comment symbols are different, they are not considered to be ---in the same group. ---@param comments table ---@param row number ---@param pos table ---@return boolean is_pattern1 function M.is_pattern1(comments, row, pos) if not util.has_key(comments, "empty") then return false end local ok, col, prefix = M._is_pattern1(comments, row) if not ok then return false end table.insert(pos, { row = row, col = col }) local function search(dir, border) local attention_row = row while true do attention_row = attention_row + dir if attention_row == border then break end ok, col = M._is_pattern1(comments, attention_row, prefix) if not ok then break end local p = { row = attention_row, col = col } if dir == -1 then table.insert(pos, 1, p) else table.insert(pos, p) end end end -- Search above search(-1, 1) -- Search below search(1, fn.line("$")) -- update pos._lines = api.nvim_buf_get_lines(0, pos[1].row - 1, pos[#pos].row, true) return true end ---Checks if a line is pattern 1, and if so, returns the range removed indentation and ---comment string. If we already known a line is pattern 1, using 'prefix' to look for ---lines above and below it that begin with the same indentation and comment string. ---@param comments table ---@param row number ---@param prefix? string ---@return boolean? is_pattern1 ---@return table? col ---@return string? prefix function M._is_pattern1(comments, row, prefix) -- 1. the comment string repeats at the start of each line (e.g. this line) local line = fn.getline(row) if prefix then return vim.startswith(line, prefix), { #prefix + 1, #line } else local indent = [[^\V\s\*]] local col_s, col_e = vim.regex(indent .. comments.empty):match_line(0, row - 1) if col_s then prefix = line:sub(col_s, col_e) return true, { #prefix + 1, #line }, prefix end end end function M.is_pattern2(comments, range, pos) if not util.has_key(comments, "empty") then return false end local line = pos._lines[1] local comment = line:sub(range[2], range[4]) local _, col_e = vim.regex("^" .. comments.empty):match_str(comment) if col_e then table.insert(pos, { row = range[1], col = { range[2] + col_e + 1, range[4] } }) return true end end function M.assert_pattern3(comments, range, pos) if not util.has_key(comments, "s", "m", "e") then error("Invalid &comments") end -- like v selection for i, line in ipairs(pos._lines) do local indent = line:match("^%s*") local p = { row = range[1] + i - 1, col = { #indent + 1, #line } } table.insert(pos, p) end pos[1].col[1] = range[2] pos[#pos].col[2] = math.min(pos[#pos].col[2], range[4]) -- Remove start of three-piece local first_line = pos._lines[1]:sub(range[2]) if vim.regex("^" .. comments.s .. [[\s\*\$]]):match_str(first_line) then -- This line is unnecessary because it is only a comment string table.remove(pos, 1) table.remove(pos._lines, 1) else local _, num_of_com = vim.regex("^" .. comments.s):match_str(first_line) if num_of_com then pos[1].col[1] = pos[1].col[1] + num_of_com else error("The start of three-piece can't found") end end -- Remove middle of three-piece if exists if #pos > 2 then for i = 2, #pos do local selected = pos._lines[i]:sub(pos[i].col[1], pos[i].col[2]) local _, num_of_com = vim.regex("^" .. comments.m):match_str(selected) -- In the case of the last line, end of three-piece may be misunderstood as middle of three-piece. if num_of_com and (i < #pos or not vim.regex("^" .. comments.e):match_str(selected)) then pos[i].col[1] = pos[i].col[1] - num_of_com end end end -- Remove end of three-piece local last_line = pos._lines[#pos._lines]:sub(1, range[4]) if vim.regex([[^\V\s\*]] .. comments.e .. [[\$]]):match_str(last_line) then -- This line is unnecessary because it is only a comment string table.remove(pos, #pos) table.remove(pos._lines, #pos._lines) else local comStart, comEnd = vim.regex(comments.e .. [[\$]]):match_str(last_line) if comStart then local num_of_com = comEnd - comStart pos[#pos].col[2] = pos[#pos].col[2] - num_of_com else error("The end of three-piece can't found") end end end function M.remove_string_symbol(symbols, range, pos) local begin_row, last_row = range[1], range[3] local begin_col, last_col = range[2], range[4] local lines = api.nvim_buf_get_lines(0, begin_row - 1, last_row, true) pos._lines = lines for i, line in ipairs(lines) do pos[i] = { row = begin_row + i - 1, col = { 1, #line } } end pos[1].col[1] = begin_col pos[#pos].col[2] = last_col for _, s in ipairs(symbols) do if vim.startswith(lines[1]:sub(begin_col), s.begin) then pos[1].col[1] = pos[1].col[1] + #s.begin - 1 pos[#pos].col[2] = pos[#pos].col[2] - #s.last if #pos >= 2 then local indent = lines[2]:match("^%s*") if #indent > 0 then for i = 2, #pos do pos[i].col[1] = #indent + 1 end end end if pos[1].col[1] == pos[1].col[2] then table.remove(pos, 1) table.remove(pos._lines, 1) end if pos[#pos].col[1] == pos[#pos].col[2] then pos[#pos] = nil pos._lines[#pos._lines] = nil end end end end return M ================================================ FILE: lua/translate/util/context.lua ================================================ local fn = vim.fn local util = require("translate.util.util") local TreeSitter = require("translate.kit.Lua.TreeSitter") local M = { vim = {}, ts = {}, } ---Get vim's syntax groups for specified position. ---NOTE: This function accepts 1-origin cursor position. ---@param cursor number[] @{lnum, col} ---@return string[]? function M.vim.get_range(group_name, cursor) if not M.vim.is_group(group_name, cursor) then return end -- Search start position local pos_s = util.tbl_copy(cursor) while true do local _pos_s = M.vim.jump(pos_s, 0) if _pos_s and M.vim.is_group(group_name, _pos_s) then pos_s = _pos_s else break end end -- Search end position local pos_e = util.tbl_copy(cursor) while true do local _pos_e = M.vim.jump(pos_e, 1) if _pos_e and M.vim.is_group(group_name, _pos_e) then pos_e = _pos_e else break end end local range = util.concat(pos_s, pos_e) return range end ---Moves to the end of the next word or the beginning of the previous word. ---@param pos number[] @{ row, col } ---@param dir integer @if 0, next, otherwise previous ---@return { row: number, col: number }? function M.vim.jump(pos, dir) local row, col = pos[1], pos[2] local current_line = fn.getline(row) if dir == 0 then -- Head of previous word col = current_line:sub(1, col - 1):find("%S+%s*$") if not col then repeat row = row - 1 if row < 1 then return end current_line = fn.getline(row) col = current_line:find("%S+%s*$") until col end else -- Tail of next word local max_row = fn.line("$") _, col = current_line:find("%S+", col + 1) if not col then -- next not empty line repeat row = row + 1 if row > max_row then return end current_line = fn.getline(row) _, col = current_line:find("%S+") until col end end return { row, col } end function M.vim.is_group(group_name, pos) for _, syntax_id in ipairs(fn.synstack(pos[1], pos[2])) do if fn.synIDattr(fn.synIDtrans(syntax_id), "name") == group_name then return true end end return false end ---Get tree-sitter's syntax groups for specified position. ---@param node_type string ---@param pos number[] (1,1)-index ---@return string[]? range function M.ts.get_range(node_type, pos) local row, col = unpack(pos) -- (1, 1) -> (0, 0) row = row - 1 col = col - 1 local node = TreeSitter.get_node_at(row, col) if node == nil then return end local parents = TreeSitter.parents(node) for _, p_node in ipairs(parents) do if p_node:type() == node_type then local s_row, s_col, e_row, e_col = p_node:range() -- From 0-index to 1-index s_row = s_row + 1 s_col = s_col + 1 e_row = e_row + 1 e_col = e_col + 1 return { s_row, s_col, e_row, e_col } end end end return M ================================================ FILE: lua/translate/util/replace.lua ================================================ local config = require("translate.config") local M = { command_name = "", } ---@param command_name string function M.set_command_name(command_name) M.command_name = command_name end ---@param lines string[] ---@param is_before boolean ---@return string[] local function replace(lines, is_before) local replace_symbols = config.get("replace_symbols") or {} local symbols = replace_symbols[M.command_name] if symbols and next(symbols) ~= nil then for i, line in ipairs(lines) do for org, rep in pairs(symbols) do if is_before then line = line:gsub(org, rep) else line = line:gsub(rep, org) end end lines[i] = line end end return lines end ---@param lines string[] ---@return string[] function M.before(lines) return replace(lines, true) end ---@param lines string[] | string ---@return string[] function M.after(lines) if type(lines) == "string" then lines = { lines } end return replace(lines, false) end return M ================================================ FILE: lua/translate/util/select.lua ================================================ local api = vim.api local fn = vim.fn local comment = require("translate.util.comment") local utf8 = require("translate.util.utf8") local util = require("translate.util.util") local M = {} local L = {} ---@class position ---@field row integer ---@field col integer[] { begin, last } ---@class positions ---@field _lines string[] ---@field _mode "comment" | "n" | "v" | "V" | "" ---@field [1] position[] ---@param args table ---@param mode string ---@return positions function M.get(args, mode) if args.comment then return comment.get_range() elseif mode == "n" then return L.get_current_line() else return L.get_visual_selected(mode) end end ---@param mode string ---@return positions function L.get_visual_selected(mode) local start, last -- When called from command line, "v" and "." return the same locations (cursor position, not selection range). -- In this case, '< and '> must be used. if util.same_pos(".", "v") then start = util.getpos("'<") last = util.getpos("'>") else start = util.getpos("v") last = util.getpos(".") end local pos_s, pos_e = util.which_front(start, last) local lines = api.nvim_buf_get_lines(0, pos_s[1] - 1, pos_e[1], true) local pos = {} pos._lines = lines pos._mode = mode if mode == "V" then for i, line in ipairs(lines) do table.insert(pos, { row = pos_s[1] + i - 1, col = { 1, #line } }) end else local last_line = fn.getline(pos_e[1]) local is_end = pos_e[2] == #last_line + 1 -- Selected to the end of each line. if not is_end then local offset = utf8.offset(last_line, 2, pos_e[2]) if offset then pos_e[2] = offset - 1 else -- The last character of the line. pos_e[2] = #last_line end end if mode == "v" then for i, line in ipairs(lines) do local p = { row = pos_s[1] + i - 1, col = { 1, #line } } table.insert(pos, p) end pos[1].col[1] = pos_s[2] pos[#pos].col[2] = pos_e[2] elseif mode == "" then for i, _ in ipairs(lines) do local row = pos_s[1] + i - 1 local col_end = is_end and #fn.getline(row) or pos_e[2] table.insert(pos, { row = row, col = { pos_s[2], col_end } }) end end end return pos end ---@return positions function L.get_current_line() local row = fn.line(".") local line = api.nvim_get_current_line() local pos = { { row = row, col = { 1, #line } } } pos._lines = { line } pos._mode = "n" return pos end return M ================================================ FILE: lua/translate/util/utf8.lua ================================================ local utf8 = {} local bit = require("bit") -- luajit local band = bit.band local bor = bit.bor local rshift = bit.rshift local lshift = bit.lshift ---The pattern (a string, not a function) "[\0-\x7F\xC2-\xF4][\x80-\xBF]*", ---which matches exactly one UTF-8 byte sequence, assuming that the subject is a valid UTF-8 string. utf8.charpattern = "[%z\x01-\x7F\xC2-\xF4][\x80-\xBF]*" ---@param idx integer ---@param func_name string ---@param range_name string ---@return string @error message local function create_errmsg(idx, func_name, range_name) return string.format("bad argument #%s to '%s' (%s out of range)", idx, func_name, range_name) end ---Converts indexes of a string to positive numbers. ---@param str string ---@param idx integer ---@return boolean, integer local function validate_range(str, idx) idx = idx > 0 and idx or #str + idx + 1 if idx < 0 or idx > #str then return false end return true, idx end ---Receives zero or more integers, converts each one to its corresponding UTF-8 byte sequence ---and returns a string with the concatenation of all these sequences. ---@vararg integer ---@return string function utf8.char(...) local buffer = {} for i, v in ipairs({ ... }) do if v < 0 or v > 0x10FFFF then error(create_errmsg(i, "char", "value"), 2) elseif v < 0x80 then -- single-byte buffer[i] = string.char(v) elseif v < 0x800 then -- two-byte local b1 = bor(0xC0, band(rshift(v, 6), 0x1F)) -- 110x-xxxx local b2 = bor(0x80, band(v, 0x3F)) -- 10xx-xxxx buffer[i] = string.char(b1, b2) elseif v < 0x10000 then -- three-byte local b1 = bor(0xE0, band(rshift(v, 12), 0x0F)) -- 1110-xxxx local b2 = bor(0x80, band(rshift(v, 6), 0x3F)) -- 10xx-xxxx local b3 = bor(0x80, band(v, 0x3F)) -- 10xx-xxxx buffer[i] = string.char(b1, b2, b3) else -- four-byte local b1 = bor(0xF0, band(rshift(v, 18), 0x07)) -- 1111-0xxx local b2 = bor(0x80, band(rshift(v, 12), 0x3F)) -- 10xx-xxxx local b3 = bor(0x80, band(rshift(v, 6), 0x3F)) -- 10xx-xxxx local b4 = bor(0x80, band(v, 0x3F)) -- 10xx-xxxx buffer[i] = string.char(b1, b2, b3, b4) end end return table.concat(buffer, "") end ---Returns the next one character range. ---@param s string ---@param start_pos integer ---@return integer start_pos, integer end_pos local function next_char(s, start_pos) local b1 = s:byte(start_pos) if not b1 then return -- for offset's #s+1 end local end_pos if band(b1, 0x80) == 0x00 then -- single-byte (0xxx-xxxx) return start_pos, start_pos elseif 0xC2 <= b1 and b1 <= 0xDF then -- two-byte (range 0xC2 to 0xDF) end_pos = start_pos + 1 elseif band(b1, 0xF0) == 0xE0 then -- three-byte (1110-xxxx) end_pos = start_pos + 2 elseif 0xF0 <= b1 and b1 <= 0xF4 then -- four-byte (range 0xF0 to 0xF4) end_pos = start_pos + 3 else -- invalid 1st byte return end -- validate (end_pos) if end_pos > #s then return end -- validate (continuation) for _, bn in ipairs({ s:byte(start_pos + 1, end_pos) }) do if band(bn, 0xC0) ~= 0x80 then -- 10xx-xxxx? return end end return start_pos, end_pos end ---Returns values so that the construction --- ---for p, c in utf8.codes(s) do body end --- ---will iterate over all UTF-8 characters in string s, with p being the position (in bytes) and c the code point of each character. ---It raises an error if it meets any invalid byte sequence. ---@param s string ---@return function iterator function utf8.codes(s) vim.validate({ s = { s, "string" }, }) local i = 1 return function() if i > #s then return end local start_pos, end_pos = next_char(s, i) if start_pos == nil then error("invalid UTF-8 code", 2) end i = end_pos + 1 return start_pos, s:sub(start_pos, end_pos) end end ---Returns the code points (as integers) from all characters in s that start between byte position i and j (both included). ---The default for i is 1 and for j is i. ---It raises an error if it meets any invalid byte sequence. ---@param s string ---@param i? integer start position. default=1 ---@param j? integer end position. default=i ---@return integer @code point function utf8.codepoint(s, i, j) vim.validate({ s = { s, "string" }, i = { i, "number", true }, j = { j, "number", true }, }) local ok ok, i = validate_range(s, i or 1) if not ok then error(create_errmsg(2, "codepoint", "initial potision"), 2) end ok, j = validate_range(s, j or i) if not ok then error(create_errmsg(3, "codepoint", "final potision"), 2) end local ret = {} repeat local char_start, char_end = next_char(s, i) if char_start == nil then error("invalid UTF-8 code", 2) end i = char_end + 1 local len = char_end - char_start + 1 if len == 1 then -- single-byte table.insert(ret, s:byte(char_start)) else -- multi-byte local b1 = s:byte(char_start) b1 = band(lshift(b1, len + 1), 0xFF) -- e.g. 110x-xxxx -> xxxx-x000 b1 = lshift(b1, len * 5 - 7) -- >> len+1 and << (len-1)*6 local cp = 0 for k = char_start + 1, char_end do local bn = s:byte(k) cp = bor(lshift(cp, 6), band(bn, 0x3F)) end cp = bor(b1, cp) table.insert(ret, cp) end until char_end >= j return unpack(ret) end ---Returns the number of UTF-8 characters in string s that start between positions i and j (both inclusive). ---The default for i is 1 and for j is -1. ---If it finds any invalid byte sequence, returns fail plus the position of the first invalid byte. ---@param s string ---@param i? integer start position. default=1 ---@param j? integer end position. default=-1 ---@return integer function utf8.len(s, i, j) vim.validate({ s = { s, "string" }, i = { i, "number", true }, j = { j, "number", true }, }) local ok ok, i = validate_range(s, i or 1) if not ok then error(create_errmsg(2, "len", "initial potision"), 2) end ok, j = validate_range(s, j or -1) if not ok then error(create_errmsg(3, "len", "final potision"), 2) end local len = 0 repeat local char_start, char_end = next_char(s, i) if char_start == nil then return nil, i end i = char_end + 1 len = len + 1 until char_end >= j return len end ---Returns the position (in bytes) where the encoding of the n-th character of s (counting from position i) starts. ---A negative n gets characters before position i. ---The default for i is 1 when n is non-negative and #s+1 otherwise, so that utf8.offset(s, -n) gets the offset of the n-th character from the end of the string. ---If the specified character is neither in the subject nor right after its end, the function returns fail. --- ---As a special case, when n is 0 the function returns the start of the encoding of the character that contains the i-th byte of s. ---@param s string ---@param n integer ---@param i? integer start position. if n >= 0, default=1, otherwise default=#s+1 ---@return integer function utf8.offset(s, n, i) vim.validate({ s = { s, "string" }, n = { n, "number" }, i = { i, "number", true }, }) i = i or n >= 0 and 1 or #s + 1 if n >= 0 or i ~= #s + 1 then local ok ok, i = validate_range(s, i) if not ok then error(create_errmsg(3, "offset", "position"), 2) end end if n == 0 then for j = i, 1, -1 do local char_start = next_char(s, j) if char_start then return char_start end end elseif n > 0 then if not next_char(s, i) then error("initial position is a continuation byte", 2) end for j = i, #s do local char_start = next_char(s, j) if char_start then n = n - 1 if n == 0 then return char_start end end end else if i ~= #s + 1 and not next_char(s, i) then error("initial position is a continuation byte", 2) end for j = i, 1, -1 do local char_start = next_char(s, j) if char_start then n = n + 1 if n == 0 then return char_start end end end end end return utf8 ================================================ FILE: lua/translate/util/util.lua ================================================ local fn = vim.fn local api = vim.api local luv = vim.loop local utf8 = require("translate.util.utf8") local M = {} ---Copy the table ---NOTE: Metatable is not considered ---@param tbl table ---@return table function M.tbl_copy(tbl) if type(tbl) ~= "table" then return tbl end local new = {} for k, v in pairs(tbl) do if type(v) == "table" then new[k] = M.tbl_copy(v) else new[k] = v end end return new end ---Concatenate two list-like tables. ---@param t1 table ---@param t2 table ---@return table function M.concat(t1, t2) local new = {} for _, v in ipairs(t1) do table.insert(new, v) end for _, v in ipairs(t2) do table.insert(new, v) end return new end ---Add an element to dict[key] ---dict is a table with an array for values. ---@param dict {any: any[]} ---@param key any ---@param elem any function M.append_dict_list(dict, key, elem) if not dict[key] then dict[key] = {} end table.insert(dict[key], elem) end function M.text_cut(text, widths) local widths_is_table = type(widths) == "table" local function get_width(row) return widths_is_table and widths[row] or widths end local lines = {} local row, col = 1, 0 local width = get_width(1) local function skip_blank_line() while width == 0 do M.append_dict_list(lines, row, "") row = row + 1 width = get_width(row) end end skip_blank_line() for p, char in utf8.codes(text) do local l = api.nvim_strwidth(char) if col + l > width then if widths_is_table and widths[row + 1] == nil then local residue = text:sub(p) M.append_dict_list(lines, row, residue) break end row = row + 1 width = get_width(row) col = 0 skip_blank_line() end M.append_dict_list(lines, row, char) col = col + l end for i, line in ipairs(lines) do lines[i] = table.concat(line, "") end if #lines == 0 then lines = { "" } end return lines end function M.max_width_in_string_list(list) local max = api.nvim_strwidth(list[1]) for i = 2, #list do local v = api.nvim_strwidth(list[i]) if v > max then max = v end end return max end function M.has_key(tbl, ...) local keys = { ... } for _, k in ipairs(keys) do if tbl[k] == nil then return false end end return true end ---@param last integer ---@return integer[][] function M.seq(last) local l = {} for i = 1, last do l[i] = { i } end return l end ---Compare position ---@param pos1 number[] #{row, col} ---@param pos2 number[] #{row, col} ---@return number[], number[] #front, end function M.which_front(pos1, pos2) -- Row Comparison if pos1[1] < pos2[1] then return pos1, pos2 elseif pos1[1] > pos2[1] then return pos2, pos1 else -- Col Comparison if pos1[2] < pos2[2] then return pos1, pos2 else return pos2, pos1 end end end ---Wrapper function for getpos() that returns only 'row' and 'col'. ---@param expr string ---@return integer[] {row, col} function M.getpos(expr) local p = vim.fn.getpos(expr) local result = { p[2], p[3] } return result end ---Returns whether cursor positions are equal. ---@param expr1 string ---@param expr2 string ---@return boolean function M.same_pos(expr1, expr2) local p1 = M.getpos(expr1) local p2 = M.getpos(expr2) return p1[1] == p2[1] and p1[2] == p2[2] end ---@param text string #json string ---@return string function M.write_temp_data(text) local dir = fn.expand(fn.stdpath("cache") .. "/translate") vim.fn.mkdir(dir, "p") local path = fn.expand(dir .. "/data.json") -- tonumber("666", 8) -> 438 local fd = assert(luv.fs_open(path, "w", 438)) assert(luv.fs_write(fd, text)) assert(luv.fs_close(fd)) return path end return M ================================================ FILE: plugin/translate.lua ================================================ if vim.g.loaded_translate_nvim then return end require("translate").setup({}) ================================================ FILE: stylua.toml ================================================ column_width = 120 line_endings = "Unix" indent_type = "Spaces" indent_width = 2 quote_style = "AutoPreferDouble" ================================================ FILE: utils/minimal.vim ================================================ let s:plug_dir = expand('/tmp/plugged/vim-plug') if !filereadable(s:plug_dir .. '/autoload/plug.vim') execute printf('!curl -fLo %s/autoload/plug.vim --create-dirs https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim', s:plug_dir) end execute 'set runtimepath+=' . s:plug_dir call plug#begin(s:plug_dir) Plug 'uga-rosa/translate.nvim' call plug#end() PlugInstall | quit lua <