Full Code of uga-rosa/translate.nvim for AI

main 1a841e56407b cached
67 files
129.4 KB
38.0k tokens
1 requests
Download .txt
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.

<details><summary>:Translate</summary><div>


    :[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 <Cmd> for mapping.
        If you cannot use it, you must change the format with nmap and xmap.


        nnoremap me <Cmd>Translate EN<CR>
        xnoremap me <Cmd>Translate EN<CR>
        
        Another way.

        nnoremap me :<C-u>Translate EN<CR>
        xnoremap me :Translate EN<CR>


</div></details>


# Translate the word under the cursor

You can use this mapping.

```vim
nnoremap <space>tw viw:Translate ZH<CR>
```


================================================
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 |<Cmd>|.

>
	nnoremap me <Cmd>Translate EN<CR>
	xnoremap me <Cmd>Translate EN<CR>
<

        If you cannot use it, you must change the format with nmap and xmap.

>
	nnoremap me :<C-u>Translate EN<CR>
	xnoremap me :Translate EN<CR>
<


==============================================================================
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 <Space> 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.
<https://curl.se/>


					*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.
	<https://github.com/soimort/translate-shell>


				*translate-nvim-preset-command-deepl-free*
	deepl_free ~

	Use DeepL API Free
	<https://www.deepl.com/en/docs-api/>

	Set your DeepL API authorization key to |g:deepl_api_auth_key|.


				*translate-nvim-preset-command-deepl-pro*
	deepl_pro ~

	Use DeepL API Pro
	<https://www.deepl.com/en/docs-api/>

	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 `<Cmd>` 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<T>: { 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<string, boolean>
---@field private entries table<string, any>
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<string, ___plugin_name___.kit.Config.SchemaInternal>
---@field private _buffer table<integer, ___plugin_name___.kit.Config.SchemaInternal>
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('<Cmd>lua require("___plugin_name___.kit.Vim.Keymap")._resolve()<CR>')
  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",
      "<Plug>(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{<Plug>(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 <https://www.deepl.com/docs-api/translating-text/>
  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 <https://cloud.google.com/translate/docs/languages>
  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 <https://github.com/soimort/translate-shell/wiki/Languages>
  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 <<EOF
require('translate').setup({
    -- Minimal configurations required to reproduce the problem.
})
EOF
Download .txt
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
Condensed preview — 67 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (145K chars).
[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "chars": 1767,
    "preview": "name: Bug Report\ndescription: Report a problem in translate.nvim\nlabels: [bug]\nbody:\n  - type: checkboxes\n    id: not-qu"
  },
  {
    "path": ".gitignore",
    "chars": 10,
    "preview": "/doc/tags\n"
  },
  {
    "path": "LICENSE",
    "chars": 1065,
    "preview": "MIT License\n\nCopyright (c) 2021 uga-rosa\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\no"
  },
  {
    "path": "README.md",
    "chars": 3767,
    "preview": "# translate.nvim\n\n![demo](https://user-images.githubusercontent.com/82267684/158013979-52c8ca49-84e1-4ca0-bf30-b8165cca9"
  },
  {
    "path": "doc/translate-nvim.txt",
    "chars": 18580,
    "preview": "*translate-nvim.txt*\t\t\tUse external translate command in nvim\n\n========================================================="
  },
  {
    "path": "lua/translate/command.lua",
    "chars": 1618,
    "preview": "local fn = vim.fn\n\nlocal config = require(\"translate.config\")\n\nlocal M = {}\n\nlocal modes = {\n  \"parse_before\",\n  \"comman"
  },
  {
    "path": "lua/translate/config.lua",
    "chars": 4325,
    "preview": "local M = {}\n\nM._preset = {\n  parse_before = {\n    natural = require(\"translate.preset.parse_before.natural\"),\n    trim "
  },
  {
    "path": "lua/translate/init.lua",
    "chars": 3415,
    "preview": "local luv = vim.loop\n\nlocal config = require(\"translate.config\")\nlocal replace = require(\"translate.util.replace\")\nlocal"
  },
  {
    "path": "lua/translate/kit/Async/AsyncTask.lua",
    "chars": 4888,
    "preview": "local Lua = require(\"___plugin_name___.kit.Lua\")\n\n---@class ___plugin_name___.kit.Async.AsyncTask<T>: { value: T }\n---@f"
  },
  {
    "path": "lua/translate/kit/Async/AsyncTask.spec.lua",
    "chars": 3492,
    "preview": "local AsyncTask = require(\"___plugin_name___.kit.Async.AsyncTask\")\n\ndescribe(\"kit.Async\", function()\n  local once = func"
  },
  {
    "path": "lua/translate/kit/Async/init.lua",
    "chars": 1646,
    "preview": "local AsyncTask = require(\"___plugin_name___.kit.Async.AsyncTask\")\n\n_G.__kit__ = _G.__kit__ or {}\n_G.__kit__.Async = _G."
  },
  {
    "path": "lua/translate/kit/Async/init.spec.lua",
    "chars": 632,
    "preview": "local Async = require(\"___plugin_name___.kit.Async\")\nlocal AsyncTask = require(\"___plugin_name___.kit.Async.AsyncTask\")\n"
  },
  {
    "path": "lua/translate/kit/Cache.lua",
    "chars": 1378,
    "preview": "---Create cache key.\n---@private\n---@param key string[]|string\n---@return string\nlocal function _key(key)\n  if type(key)"
  },
  {
    "path": "lua/translate/kit/Cache.spec.lua",
    "chars": 1130,
    "preview": "local Cache = require(\"___plugin_name___.kit.Cache\")\n\ndescribe(\"kit.Cache\", function()\n  it(\"should works {get,set,has,d"
  },
  {
    "path": "lua/translate/kit/Config.lua",
    "chars": 3612,
    "preview": "local kit = require(\"___plugin_name___.kit\")\nlocal Cache = require(\"___plugin_name___.kit.Cache\")\n\n---@class ___plugin_n"
  },
  {
    "path": "lua/translate/kit/Config.spec.lua",
    "chars": 1574,
    "preview": "local Config = require(\"___plugin_name___.kit.Config\")\n\ndescribe(\"kit.Config\", function()\n  before_each(function()\n    v"
  },
  {
    "path": "lua/translate/kit/LSP/Position.lua",
    "chars": 3901,
    "preview": "local Buffer = require(\"___plugin_name___.kit.Vim.Buffer\")\n\n---@class ___plugin_name___.kit.LSP.Position\n---@field publi"
  },
  {
    "path": "lua/translate/kit/LSP/Position.spec.lua",
    "chars": 1256,
    "preview": "local Position = require(\"___plugin_name___.kit.LSP.Position\")\n\ndescribe(\"kit.LSP.Position\", function()\n  local text = \""
  },
  {
    "path": "lua/translate/kit/LSP/Range.lua",
    "chars": 1123,
    "preview": "local Position = require(\"___plugin_name___.kit.LSP.Position\")\n\n---@class ___plugin_name___.kit.LSP.Range\n---@field publ"
  },
  {
    "path": "lua/translate/kit/LSP/Range.spec.lua",
    "chars": 435,
    "preview": "local Range = require(\"___plugin_name___.kit.LSP.Range\")\n\ndescribe(\"kit.LSP.Range\", function()\n  it(\"should return the r"
  },
  {
    "path": "lua/translate/kit/Lua/TreeSitter.lua",
    "chars": 7606,
    "preview": "local TreeSitter = {}\n\n---@alias ___plugin_name___.kit.Lua.TreeSitter.VisitStatus 'stop'|'skip'\nTreeSitter.VisitStatus ="
  },
  {
    "path": "lua/translate/kit/Lua/TreeSitter.spec.lua",
    "chars": 1925,
    "preview": "---@diagnostic disable: need-check-nil, param-type-mismatch\nlocal helper = require(\"kit.helper\")\nlocal TreeSitter = requ"
  },
  {
    "path": "lua/translate/kit/Lua/init.lua",
    "chars": 229,
    "preview": "local Lua = {}\n\n---Create gabage collection detector.\n---@param callback fun(...: any): any\n---@return userdata\nfunction"
  },
  {
    "path": "lua/translate/kit/Lua/init.spec.lua",
    "chars": 374,
    "preview": "local Lua = require(\"___plugin_name___.kit.Lua\")\n\ndescribe(\"kit.Lua\", function()\n  it(\"should detect gc timing.\", functi"
  },
  {
    "path": "lua/translate/kit/Vim/Buffer.lua",
    "chars": 1560,
    "preview": "local kit = require(\"___plugin_name___.kit\")\nlocal Highlight = require(\"___plugin_name___.kit.Vim.Highlight\")\n\nlocal Buf"
  },
  {
    "path": "lua/translate/kit/Vim/Buffer.spec.lua",
    "chars": 1456,
    "preview": "local Buffer = require(\"___plugin_name___.kit.Vim.Buffer\")\n\ndescribe(\"kit.Vim.Buffer\", function()\n  before_each(function"
  },
  {
    "path": "lua/translate/kit/Vim/Highlight.lua",
    "chars": 1236,
    "preview": "local kit = require(\"___plugin_name___.kit\")\nlocal Async = require(\"___plugin_name___.kit.Async\")\nlocal AsyncTask = requ"
  },
  {
    "path": "lua/translate/kit/Vim/Highlight.spec.lua",
    "chars": 285,
    "preview": "local Highlight = require(\"___plugin_name___.kit.Vim.Highlight\")\n\ndescribe(\"kit.Vim.Highlight\", function()\n  it(\"should "
  },
  {
    "path": "lua/translate/kit/Vim/Keymap.lua",
    "chars": 1130,
    "preview": "local AsyncTask = require(\"___plugin_name___.kit.Async.AsyncTask\")\n\nlocal Keymap = {}\n\nKeymap._callbacks = {}\n\n---Replac"
  },
  {
    "path": "lua/translate/kit/Vim/Keymap.spec.lua",
    "chars": 749,
    "preview": "local Async = require(\"___plugin_name___.kit.Async\")\nlocal Keymap = require(\"___plugin_name___.kit.Vim.Keymap\")\n\nlocal a"
  },
  {
    "path": "lua/translate/kit/Vim/Syntax.lua",
    "chars": 1199,
    "preview": "local kit = require(\"___plugin_name___.kit\")\n\nlocal Syntax = {}\n\n---Get all syntax groups for specified position.\n---NOT"
  },
  {
    "path": "lua/translate/kit/Vim/Syntax.spec.lua",
    "chars": 1028,
    "preview": "local helper = require(\"kit.helper\")\nlocal Syntax = require(\"___plugin_name___.kit.Vim.Syntax\")\n\ndescribe(\"kit.Vim.Synta"
  },
  {
    "path": "lua/translate/kit/init.lua",
    "chars": 3634,
    "preview": "--[[\nMIT License\n\nCopyright (c) 2022 hrsh7th\n\nPermission is hereby granted, free of charge, to any person obtaining a co"
  },
  {
    "path": "lua/translate/kit/init.spec.lua",
    "chars": 1838,
    "preview": "local kit = require(\"___plugin_name___.kit\")\n\ndescribe(\"kit\", function()\n  describe(\".merge\", function()\n    it(\"should "
  },
  {
    "path": "lua/translate/preset/command/deepl.lua",
    "chars": 1305,
    "preview": "local M = {}\n\nlocal json_encode = vim.json and vim.json.encode or vim.fn.json_encode\n\n---@param url string\n---@param lin"
  },
  {
    "path": "lua/translate/preset/command/deepl_free.lua",
    "chars": 555,
    "preview": "local deepl = require(\"translate.preset.command.deepl\")\n\nlocal M = {}\n\n---@param lines string[]\n---@param command_args t"
  },
  {
    "path": "lua/translate/preset/command/deepl_pro.lua",
    "chars": 549,
    "preview": "local deepl = require(\"translate.preset.command.deepl\")\n\nlocal M = {}\n\n---@param lines string[]\n---@param command_args t"
  },
  {
    "path": "lua/translate/preset/command/google.lua",
    "chars": 2231,
    "preview": "local util = require(\"translate.util.util\")\n\nlocal M = {}\n\nM.url =\n  \"https://script.google.com/macros/s/AKfycbxLRZgWI3U"
  },
  {
    "path": "lua/translate/preset/command/translate_shell.lua",
    "chars": 3406,
    "preview": "local M = {}\n\n---@param lines string[]\n---@param command_args table\n---@return string\n---@return string[]\nfunction M.cmd"
  },
  {
    "path": "lua/translate/preset/output/floating.lua",
    "chars": 1141,
    "preview": "local api = vim.api\n\nlocal util = require(\"translate.util.util\")\n\nlocal M = {\n  window = {},\n}\n\nfunction M.cmd(lines, _)"
  },
  {
    "path": "lua/translate/preset/output/insert.lua",
    "chars": 609,
    "preview": "local api = vim.api\n\nlocal M = {}\n\nfunction M.cmd(lines, pos)\n  if type(lines) == \"string\" then\n    lines = { lines }\n  "
  },
  {
    "path": "lua/translate/preset/output/register.lua",
    "chars": 459,
    "preview": "local fn = vim.fn\n\nlocal M = {}\n\n---Set the register\n---@param lines string[]\nfunction M.cmd(lines, _)\n  local newline\n "
  },
  {
    "path": "lua/translate/preset/output/replace.lua",
    "chars": 432,
    "preview": "local api = vim.api\n\nlocal M = {}\n\nfunction M.cmd(lines, pos)\n  if type(lines) == \"string\" then\n    lines = { lines }\n  "
  },
  {
    "path": "lua/translate/preset/output/split.lua",
    "chars": 2067,
    "preview": "local fn = vim.fn\nlocal api = vim.api\n\nlocal M = {}\n\nfunction M.cmd(lines, pos)\n  if type(lines) == \"string\" then\n    li"
  },
  {
    "path": "lua/translate/preset/parse_after/deepl.lua",
    "chars": 348,
    "preview": "local M = {}\n\nlocal json_decode = vim.json and vim.json.decode or vim.fn.json_decode\n\n---@param response string #json st"
  },
  {
    "path": "lua/translate/preset/parse_after/deepl_free.lua",
    "chars": 53,
    "preview": "return require(\"translate.preset.parse_after.deepl\")\n"
  },
  {
    "path": "lua/translate/preset/parse_after/deepl_pro.lua",
    "chars": 53,
    "preview": "return require(\"translate.preset.parse_after.deepl\")\n"
  },
  {
    "path": "lua/translate/preset/parse_after/google.lua",
    "chars": 83,
    "preview": "local M = {}\n\nfunction M.cmd(text, _)\n  return vim.json.decode(text)\nend\n\nreturn M\n"
  },
  {
    "path": "lua/translate/preset/parse_after/head.lua",
    "chars": 1461,
    "preview": "local api = vim.api\n\nlocal util = require(\"translate.util.util\")\n\nlocal M = {}\n\n---Cut the results of translation to fit"
  },
  {
    "path": "lua/translate/preset/parse_after/no_handle.lua",
    "chars": 68,
    "preview": "local M = {}\n\nfunction M.cmd(lines, _)\n  return lines\nend\n\nreturn M\n"
  },
  {
    "path": "lua/translate/preset/parse_after/oneline.lua",
    "chars": 151,
    "preview": "local M = {}\n\n---@param lines string[]\n---@return string[]\nfunction M.cmd(lines, _)\n  lines = { table.concat(lines, \"\") "
  },
  {
    "path": "lua/translate/preset/parse_after/rate.lua",
    "chars": 969,
    "preview": "local api = vim.api\n\nlocal util = require(\"translate.util.util\")\n\nlocal M = {}\n\n---Cut the results of translation to fit"
  },
  {
    "path": "lua/translate/preset/parse_after/translate_shell.lua",
    "chars": 347,
    "preview": "local M = {}\n\n---@param text string\n---@return string[]\nfunction M.cmd(text, _)\n  local crlf\n  -- Remove the extra CRLF "
  },
  {
    "path": "lua/translate/preset/parse_after/window.lua",
    "chars": 551,
    "preview": "local api = vim.api\n\nlocal util = require(\"translate.util.util\")\n\nlocal M = {}\n\n---Cut the text to fit the window width."
  },
  {
    "path": "lua/translate/preset/parse_before/concat.lua",
    "chars": 256,
    "preview": "local M = {}\n\n---@param lines string[]\n---@return string[]\nfunction M.cmd(lines)\n  local options = require(\"translate.co"
  },
  {
    "path": "lua/translate/preset/parse_before/natural.lua",
    "chars": 2666,
    "preview": "local util = require(\"translate.util.util\")\n\nlocal M = {}\n\nlocal function inc(tbl, index)\n  if tbl[index] then\n    retur"
  },
  {
    "path": "lua/translate/preset/parse_before/no_handle.lua",
    "chars": 110,
    "preview": "local M = {}\n\n---@param lines string[]\n---@return string[]\nfunction M.cmd(lines)\n  return lines\nend\n\nreturn M\n"
  },
  {
    "path": "lua/translate/preset/parse_before/trim.lua",
    "chars": 379,
    "preview": "local M = {}\n\n---@param lines string[]\n---@param pos positions\n---@return string[]\nfunction M.cmd(lines, pos)\n  for i, l"
  },
  {
    "path": "lua/translate/util/comment.lua",
    "chars": 9098,
    "preview": "local fn = vim.fn\nlocal api = vim.api\n\nlocal context = require(\"translate.util.context\")\nlocal util = require(\"translate"
  },
  {
    "path": "lua/translate/util/context.lua",
    "chars": 2954,
    "preview": "local fn = vim.fn\n\nlocal util = require(\"translate.util.util\")\nlocal TreeSitter = require(\"translate.kit.Lua.TreeSitter\""
  },
  {
    "path": "lua/translate/util/replace.lua",
    "chars": 1015,
    "preview": "local config = require(\"translate.config\")\n\nlocal M = {\n  command_name = \"\",\n}\n\n---@param command_name string\nfunction M"
  },
  {
    "path": "lua/translate/util/select.lua",
    "chars": 2522,
    "preview": "local api = vim.api\nlocal fn = vim.fn\n\nlocal comment = require(\"translate.util.comment\")\nlocal utf8 = require(\"translate"
  },
  {
    "path": "lua/translate/util/utf8.lua",
    "chars": 8243,
    "preview": "local utf8 = {}\n\nlocal bit = require(\"bit\") -- luajit\n\nlocal band = bit.band\nlocal bor = bit.bor\nlocal rshift = bit.rshi"
  },
  {
    "path": "lua/translate/util/util.lua",
    "chars": 3818,
    "preview": "local fn = vim.fn\nlocal api = vim.api\nlocal luv = vim.loop\nlocal utf8 = require(\"translate.util.utf8\")\n\nlocal M = {}\n\n--"
  },
  {
    "path": "plugin/translate.lua",
    "chars": 81,
    "preview": "if vim.g.loaded_translate_nvim then\n  return\nend\n\nrequire(\"translate\").setup({})\n"
  },
  {
    "path": "stylua.toml",
    "chars": 114,
    "preview": "column_width = 120\nline_endings = \"Unix\"\nindent_type = \"Spaces\"\nindent_width = 2\nquote_style = \"AutoPreferDouble\"\n"
  },
  {
    "path": "utils/minimal.vim",
    "chars": 500,
    "preview": "let s:plug_dir = expand('/tmp/plugged/vim-plug')\nif !filereadable(s:plug_dir .. '/autoload/plug.vim')\n  execute printf('"
  }
]

About this extraction

This page contains the full source code of the uga-rosa/translate.nvim GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 67 files (129.4 KB), approximately 38.0k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!