106 Commits

Author SHA1 Message Date
36865cc17d chore: add warning message 2023-04-07 19:57:52 +08:00
b12bef50f9 fix: know bug can't detect table key extended correctly 2023-03-29 14:04:43 +08:00
29f12aa107 fix: fix toggle entry mistake 2023-03-29 12:39:49 +08:00
8c43260cbb test: add buffer deleteline test and buffer lines test, fix bugs with that 2023-03-29 12:00:01 +08:00
89e2daf2cd test: add simple test for buffer:setline() 2023-03-29 09:41:18 +08:00
f11cee0b5d fix: fix toggle entry mistake 2023-03-29 09:19:14 +08:00
5b4ec8ea4e feat: execute toggle_entry if translate'string' window is available 2023-03-28 23:08:22 +08:00
2246968d90 docs: add busted meta doc for type hint 2023-03-28 19:56:26 +08:00
37cd2b13d9 Add z-index to hover window and refactor buffer:setline() tests in buffer_spec.lua 2023-03-28 18:00:57 +08:00
0ef08fbd19 chore: try to fix ci mistakes 2023-03-27 12:27:54 +08:00
0fa87770e7 chore: fix lua doc type definition for buffer[number] 2023-03-27 11:43:07 +08:00
8f6e871a55 fix: fix test.yaml order 2023-03-27 11:39:05 +08:00
3bc21a4923 chore: add github action for test 2023-03-27 11:28:43 +08:00
a0dbca1e7a docs: add wiki link for README 2023-03-27 11:28:15 +08:00
89d7e8f439 docs: update readme.md 2023-03-27 11:28:15 +08:00
3c2dff75e2 docs: update README.md demo.mp4 2023-03-27 11:28:15 +08:00
cee8e5107a docs: add star history for readme.md 2023-03-27 11:28:15 +08:00
7f1f8aa262 docs: add simple explain readme 2023-03-27 11:28:15 +08:00
eacb06cd30 test: add simple test for buffer:setline() 2023-03-27 10:28:03 +08:00
c5216e2b37 feat: add prompt for waitting window 2023-03-26 20:14:31 +08:00
529a1639c6 chore: sync progress 2023-03-25 09:55:29 +08:00
d2f00c8773 fix: when download abort then continue last time 2023-03-24 17:02:58 +08:00
a4dff90064 refactor: remove auto setup frontend keymap 2023-03-24 11:09:04 +08:00
82fa2a008a style: code format use lua_ls 2023-03-24 00:56:36 +08:00
9f5a81249c fix: fix wrong display size caculate remove util.display_size func 2023-03-24 00:10:14 +08:00
b44ab4bf2e refactor: rewrite TransNode and use main_loop in process instead of buffer function 2023-03-23 23:19:19 +08:00
9b8c091a33 feat: add split_width for better sentence display 2023-03-23 20:24:17 +08:00
a3b0a21d08 refactor: remove conf.query, use conf.frontend.query 2023-03-23 17:31:02 +08:00
6def81f2c6 refactor: change default dict dir to plugin dir/extra, change Trans.json style 2023-03-23 15:12:49 +08:00
84e06a268e refactor: rewrite TransNode and use main_loop in process instead of buffer function 2023-03-23 10:17:41 +08:00
8e9ccfc8f7 refactor: remove TransMode, use vim-mode 2023-03-22 23:19:31 +08:00
e90b55d297 feat: add TranslateInput command 2023-03-22 22:41:39 +08:00
2bb4b4d46b feat: add TranslateInput command 2023-03-22 22:27:27 +08:00
34a01920a5 chore: add lsp doc 2023-03-22 21:55:08 +08:00
f63574026a refactor: try to add unit test 2023-03-22 14:37:44 +08:00
8be4fb7303 fix: fix toggle entry will trigger auto_close events 2023-03-19 20:15:36 +08:00
3eae6a40e9 fix: fix backend order mistake 2023-03-18 23:49:01 +08:00
52d2741804 refactor: use online query method template instead of backend.query todo online query 2023-03-18 23:48:54 +08:00
b2cafe3448 feat: add free backend iciba.lua and try to use template method for querying backend data 2023-03-18 23:48:54 +08:00
46c69fb758 style: code format 2023-03-18 13:53:09 +08:00
a6a5a33bff fix: fix typo and add debug print option 2023-03-17 22:30:52 +08:00
e56267ea8c feat: more icon support 2023-03-17 10:38:26 +08:00
641a90de6b chore(doc): Auto generate docs 2023-03-17 02:18:27 +00:00
aabe1f3a8f feat: more icon support 2023-03-17 09:31:11 +08:00
1367537926 feat: remove duplicate strs in web definition 2023-03-17 09:27:02 +08:00
9e74b5454e chore(doc): Auto generate docs 2023-03-17 00:17:36 +00:00
ee7e3b028f fix: fix miss ',' and better youdao web format style 2023-03-17 08:16:55 +08:00
4f68e4189f chore(doc): Auto generate docs 2023-03-17 00:15:00 +00:00
b5cae7bed1 Merge pull request #31 from felixonmars/patch-1
Correct typos in init.lua
2023-03-17 08:14:25 +08:00
c17e8adb80 Correct typos in init.lua 2023-03-17 02:10:41 +02:00
75fdfc6880 chore: sync 2023-03-16 22:59:42 +08:00
b9abc99086 fix: use api.nvim_buf_get_lines instead of fn.getbufoneline 2023-03-16 21:15:00 +08:00
4a0f9b5c85 fix: try to fix install.lua mistakes 2023-03-16 19:35:08 +08:00
0d4eec6f2e feat: begin to render youdao Translation window 2023-03-16 17:17:30 +08:00
870be41bee fix: fix youdao input key calculate mistake (unicode uncode) 2023-03-16 16:07:53 +08:00
020876d8c4 chore(doc): Auto generate docs 2023-03-16 03:50:07 +00:00
9a2d3b4e0a fix: use backend local strategy instead of global strategy 2023-03-16 11:49:26 +08:00
978677696e feat: add youdao backend file 2023-03-16 00:03:13 +08:00
efec62d144 fix: return the latest instance instead of the first instance 2023-03-15 23:10:58 +08:00
df52f2339c refactor: remove Trans.define and see offline as first backend 2023-03-15 22:24:41 +08:00
3981167d38 fix: fix lua invoke c function use main event loop 2023-03-15 22:12:55 +08:00
53f1998261 chore(doc): Auto generate docs 2023-03-15 12:58:29 +00:00
af4bb42d59 refactor: remove do_query function 2023-03-15 20:57:28 +08:00
21351b3a26 chore: use conf.theme instead of conf.style.theme 2023-03-15 19:27:21 +08:00
52238cb1e7 fix: add auto_close_autocmd 2023-03-15 16:07:07 +08:00
c699aaba24 fix: fix cacha miss 2023-03-15 14:30:01 +08:00
fff23d4f41 chore(doc): Auto generate docs 2023-03-15 04:01:12 +00:00
da8b037523 feat: add auto_resize config for hover window 2023-03-15 12:00:37 +08:00
e5f0f2bf34 feat: add auto resize animation 2023-03-15 11:34:50 +08:00
baf30a3db1 fix: fix setline mistake 2023-03-15 00:18:31 +08:00
f47abd1691 fix: more lsp info comment and fix backend opts error 2023-03-14 22:30:45 +08:00
dab105b27f chore(doc): Auto generate docs 2023-03-14 10:21:05 +00:00
8a0021ead7 docs: add function explain doc 2023-03-14 18:17:07 +08:00
4931bdc74a fix: backup 2023-03-14 13:18:53 +08:00
2c490994db chore(doc): Auto generate docs 2023-03-13 14:43:26 +00:00
02fe6c510a fix: fix buffer line index error and online process need to be fixed 2023-03-13 22:39:40 +08:00
693dd0c7aa refactor: add frontend.hover.load function to set buffer content 2023-03-13 21:44:56 +08:00
7731c6c8cb refactor: remove wrapper folder 2023-03-13 19:50:28 +08:00
1fe20004ec refactor: begin to refactor buffer node and buffer obj 2023-03-13 11:51:46 +08:00
9d0aca954d refactor: implement waitting window animation 2023-03-12 23:24:32 +08:00
3c28af5314 refactor: begin to try to refactor frontend part 2023-03-12 22:28:15 +08:00
f86ff7b615 refactor: add cache logic 2023-03-12 21:33:00 +08:00
bc8c673ee0 fix: change global config to default option 2023-03-12 20:09:08 +08:00
493fad6a3f fix: fix curl GET method can convert space to arguments 2023-03-12 16:24:21 +08:00
5a36ffad1c fix: better backend configuration file parser 2023-03-12 15:18:38 +08:00
ded28b68bc style: use Trans.key instead of "Trans.key" 2023-03-12 11:23:29 +08:00
b05018e1bf chore(doc): Auto generate docs 2023-03-12 01:57:01 +00:00
27ef2c00b0 chore: sync and leave bug to be fixed 2023-03-12 09:56:31 +08:00
cdb1ab847a chore(doc): Auto generate docs 2023-03-11 03:33:02 +00:00
164e17f737 chore: sync 2023-03-11 11:32:13 +08:00
20fffe0ee5 style(core.*, health.lua): better code style and backend strategy 2023-03-11 00:25:21 +08:00
7fd7d1c309 Auto generate docs 2023-03-10 03:35:52 +00:00
5457809eef refactor: add -L option for install stardict 2023-03-10 11:35:14 +08:00
8dd538ba60 refactor: try to remove dependency for plenary.curl 2023-03-10 11:23:22 +08:00
a5164bf052 Auto generate docs 2023-03-10 01:16:36 +00:00
35807247ff refactor: use install.lua instead of install.sh 2023-03-10 09:15:53 +08:00
da1f847bd0 refactor: support async online query engine:baidu 2023-03-10 07:25:20 +08:00
eb6e93c24b Auto generate docs 2023-03-09 11:53:04 +00:00
0be7ff07b5 feat: try to use panvimdoc 2023-03-09 19:52:22 +08:00
69ac7653bf refactor: use corountine for async process 2023-03-08 23:34:23 +08:00
947beb16c8 chore: sync 2023-03-08 15:46:30 +08:00
8f6b1d4069 refactor: remove useless code 2023-03-08 11:53:41 +08:00
831108a316 refactor: misc 2023-03-08 09:44:37 +08:00
e9f5f7f160 chore: add misc file 2023-03-07 22:58:14 +08:00
7dbf3b17be refactor: better source code framework 2023-03-04 22:46:57 +08:00
cbf47ebdaa fix: fix window is not valid 2023-02-18 13:25:39 +08:00
53 changed files with 3711 additions and 1826 deletions

View File

@ -1,7 +0,0 @@
version = 1
[[analyzers]]
name = "shell"
[[analyzers]]
name = "javascript"

51
.github/workflows/test.yaml vendored Normal file
View File

@ -0,0 +1,51 @@
jobs:
run_tests:
name: unit tests
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- run: date +%F > todays-date
- name: Restore cache for today's nightly.
uses: actions/cache@v3
with:
key: ${{ runner.os }}-${{ matrix.rev }}-${{ hashFiles('todays-date') }}
path: _neovim
- name: Prepare
run: |
test -d _neovim || {
mkdir -p _neovim
curl -sL "https://github.com/neovim/neovim/releases/download/${{ matrix.rev }}" | tar xzf - --strip-components=1 -C "${PWD}/_neovim"
}
mkdir -p ~/.local/share/nvim/site/pack/vendor/start
git clone --depth 1 https://github.com/nvim-lua/plenary.nvim ~/.local/share/nvim/site/pack/vendor/start/plenary.nvim
ln -s $(pwd) ~/.local/share/nvim/site/pack/vendor/start
- name: Run tests
run: |
export PATH="${PWD}/_neovim/bin:${PATH}"
export VIM="${PWD}/_neovim/share/nvim/runtime"
nvim --version
make test
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-22.04
rev: nightly/nvim-linux64.tar.gz
- os: ubuntu-22.04
rev: v0.7.2/nvim-linux64.tar.gz
- os: ubuntu-22.04
rev: v0.8.2/nvim-linux64.tar.gz
name: Test
on:
pull_request:
paths:
- "lua/**"
- "plugin/**"
- "script/*"
- "makefile"
push:
paths:
- "lua/**"
- "plugin/**"
- "script/*"
- "makefile"

3
.gitignore vendored
View File

@ -4,3 +4,6 @@ demo.mp4
screenshot.gif
tts/node_modules/
tts/package-lock.json
Trans.json
ultimate.db
lua/.luarc.json

384
README.md
View File

@ -2,22 +2,24 @@
<!--toc:start-->
- [Trans.nvim](#transnvim)
- [Trans.nvim](#transnvim) - [注意: 当前分支目前没有发布, README.md 的描述并不准确, 遇到问题请切换到 `main`分支或者联系我](#注意-当前分支目前没有发布-readmemd-的描述并不准确-遇到问题请切换到-main分支或者联系我)
- [特点](#特点)
- [屏幕截图](#屏幕截图)
- [演示](#演示)
- [离线查询](#离线查询)
- [\*在线查询\*\* (有道)](#在线查询-有道)
- [主题](#主题)
- [安装](#安装)
- [配置](#配置)
- [快捷键绑定](#快捷键绑定)
- [高亮组](#高亮组)
- [声明](#声明)
- [感谢](#感谢)
- [贡献](#贡献)
- [待办 (画大饼)](#待办-画大饼)
- [项目情况](#项目情况)
<!--toc:end-->
## 注意: 此分支已经不再打算维护, 新版本目前在expermental分支, 稳定后会设置成默认分支
### 注意: 当前分支目前没有发布, README.md 的描述并不准确, 遇到问题请切换到 `main`分支或者联系我
## 特点
@ -30,43 +32,42 @@
> 见 wiki
- 大部分功能可以自定义:
- 高亮
- 悬浮大小
- 排版顺序
- 弹窗大小
- `舒服窗口动画`
- etc (更多可以查看[配置](#配置))
- **完全离线** 的单词翻译体验 (可能后面会支持在线翻译)
- 支持显示:
- 柯林斯星级
- 牛津 3000 词汇
- 中文翻译
- 英文翻译 (不是英译中, 而是用英文解释)
- 词根
- etc
- 舒服的排版和`动画`
- 支持 `normal``visual`模式
> <font color='#FF9900'>不支持 visual-block mode</font>
- 本地词库单词量: `430w`
## 屏幕截图
### 演示
> 视频演示的在线查询, 查询速度取决于你的网络状况
> 可以打开音量查看自动读音
> 可以点开声音查看离线自动发音
https://user-images.githubusercontent.com/107862700/215941500-3293c571-20a1-44e2-b202-77079f158ce9.mp4
### 离线查询
https://user-images.githubusercontent.com/107862700/213752097-2eee026a-ddee-4531-bf80-ba2cbc8b44ef.mp4
https://user-images.githubusercontent.com/107862700/226175984-1a95bea7-8d66-450e-87e1-ba9c91c37ab8.mp4
### 在线查询 (有道)
https://user-images.githubusercontent.com/107862700/226176106-c2962dd3-d66c-499c-b44a-1f471b79fe38.mp4
**使用在线查询需要配置相应的 app_id 和 app_passwd**
在线查询配置见: [wiki](https://github.com/JuanZoran/Trans.nvim/wiki/%E9%85%8D%E7%BD%AE#%E5%9C%A8%E7%BA%BF%E6%9F%A5%E8%AF%A2%E9%85%8D%E7%BD%AE)
### 主题
@ -87,7 +88,7 @@ https://user-images.githubusercontent.com/107862700/213752097-2eee026a-ddee-4531
_安装之前, 首先需要明确本插件的依赖:_
- [ECDICT](https://github.com/skywind3000/ECDICT): 插件所用的离线单词数据库
- sqlite.lua: 操作数据库所用的库
- [sqlite.lua](https://github.com/kkharji/sqlite.lua): 操作数据库所用的库
- sqlite3: 数据库
<details>
@ -96,8 +97,8 @@ _安装之前, 首先需要明确本插件的依赖:_
```lua
use {
'JuanZoran/Trans.nvim'
run = 'bash ./install.sh',
requires = 'kkharji/sqlite.lua',
run = function() require('Trans').install() end, -- 自动下载使用的本地词库
requires = 'kkharji/sqlite.lua', ,
-- 如果你不需要任何配置的话, 可以直接按照下面的方式启动
config = function ()
require'Trans'.setup{
@ -117,13 +118,15 @@ use {
{ {'n', 'x'}, 'mk' },
{ 'n', 'mi' },
},
run = 'bash ./install.sh', -- 自动下载使用的本地词库
requires = 'kkharji/sqlite.lua',
run = function() require('Trans').install() end, -- 自动下载使用的本地词库
requires = { 'kkharji/sqlite.lua', },
config = function()
require("Trans").setup {} -- 启动Trans
vim.keymap.set({"n", 'x'}, "mm", '<Cmd>Translate<CR>', { desc = ' Translate' }) -- 自动判断virtual 还是 normal 模式
vim.keymap.set({'n', 'x'}, 'mk', '<Cmd>TransPlay<CR>', {desc = ' 自动发音'}) -- 自动发音选中或者光标下的单词
vim.keymap.set("n", "mi", "<Cmd>TranslateInput<CR>", { desc = ' Translate' })
require("Trans").setup {
-- your configuration here
}
vim.keymap.set({"n", 'x'}, "mm", '<Cmd>Translate<CR>') -- 自动判断visual 还是 normal 模式
vim.keymap.set({'n', 'x'}, 'mk', '<Cmd>TransPlay<CR>') -- 自动发音选中或者光标下的单词
vim.keymap.set('n', 'mi', '<Cmd>TransInput<CR>')
end
}
```
@ -139,12 +142,11 @@ use {
keys = {
-- 可以换成其他你想映射的键
{ 'mm', mode = { 'n', 'x' }, '<Cmd>Translate<CR>', desc = ' Translate' },
{ 'mk', mode = { 'n', 'x' }, '<Cmd>TransPlay<CR>', desc = ' 自动发音' },
{ 'mk', mode = { 'n', 'x' }, '<Cmd>TransPlay<CR>', desc = ' Auto Play' },
-- 目前这个功能的视窗还没有做好可以在配置里将view.i改成hover
{ 'mi', '<Cmd>TranslateInput<CR>', desc = ' Translate From Input' },
},
dependencies = { 'kkharji/sqlite.lua', lazy = true },
dependencies = { 'kkharji/sqlite.lua', },
opts = {
-- your configuration there
}
@ -155,31 +157,28 @@ use {
<font color="#FF9900">**注意事项**: </font>
- **如果插件无法正常工作, 请运行**`:check Trans`, 查看插件是否安装正确并且处于正常工作环境
- `install.sh`
- 使用了 `wget`下载词库, 安装请确保你的环境变量中存在 wget
- install.sh 下载后会自动将词库解压, 并移动到 `$HOME/.vim/dict`文件夹下
- 目前仅在 `Ubuntu22.04`的环境下测试通过
> 如果上述条件不符合, 请删掉 `run = 'install.sh'`部分, 考虑手动安装词库
> 如果上述条件满足, 仍出现问题, 欢迎在 issue 里向我反馈,我会及时尝试解决
- 下载词典的过程中, 需要能够 `流畅的访问github下载`
> 词库文件压缩包大小为: **281M**
> 解压缩后的大小大概为: **1.2G**
> 词库文件压缩包大小为: **281M**
> 解压缩后的大小大概为: 1.2G
- 安装后如果不能正常运行, 请尝试检查一下问题:
- 本机是否已经安装了 `sqlite3`
> Linux 下安装:
> Linux 下安装:
> `sudo pacman -S sqlite # Arch`
> `sudo apt-get install sqlite3 libsqlite3-dev # Ubuntu`
> **尝试运行 `checkhealth Trans`**
- **`auto_play`** 使用步骤:
> linux 只需要安装`festival`
@ -187,15 +186,13 @@ use {
> **_如果你想要设置音色发音可以访问:_** [Festival 官方](https://www.cstr.ed.ac.uk/projects/festival/morevoices.html)
> 可以选择英音、美音、男声、女声
> mac 系统使用`say` (感谢[@happysmile12321](https://github.com/happysmile12321) )
> 其他操作系统
- 需要确保安装了`nodejs`
- 进入插件的`tts`目录运行`npm install`
> 如果`install.sh`运行正常则自动安装,如果安装失败,请尝试手动安装
> 如果`install`运行正常则自动安装,如果安装失败,请尝试手动安装
- `title`的配置,只对`neovim 0.9`版本有效
- `title`的配置,只对`neovim 0.9+`版本有效
<details>
<summary>Festival配置(仅针对linux用户)</summary>
@ -247,112 +244,109 @@ use {
## 配置
详细见**wiki**: [配置说明](https://github.com/JuanZoran/Trans.nvim/wiki/%E9%85%8D%E7%BD%AE)
```lua
require'Trans'.setup {
view = {
i = 'float',
n = 'hover',
v = 'hover',
},
hover = {
width = 37,
height = 27,
border = 'rounded',
title = title,
keymap = {
pageup = '[[',
pagedown = ']]',
pin = '<leader>[',
close = '<leader>]',
toggle_entry = '<leader>;',
play = '_',
require 'Trans'.setup {
---@type string the directory for database file and password file
dir = require 'Trans'.plugin_dir,
---@type 'default' | 'dracula' | 'tokyonight' global Trans theme [see lua/Trans/style/theme.lua]
theme = 'default', -- default | tokyonight | dracula
strategy = {
---@type { frontend:string, backend:string | string[] } fallback strategy for mode
default = {
frontend = 'hover',
backend = '*',
},
animation = {
-- open = 'fold',
-- close = 'fold',
open = 'slid',
close = 'slid',
interval = 12,
},
auto_close_events = {
'InsertEnter',
'CursorMoved',
'BufLeave',
},
auto_play = true,
timeout = 3000,
spinner = 'dots', -- 查看所有样式: /lua/Trans/util/spinner
-- spinner = 'moon'
},
float = {
width = 0.8,
height = 0.8,
border = 'rounded',
title = title,
keymap = {
quit = 'q',
---@type table frontend options
frontend = {
---@class TransFrontendOpts
---@field keymaps table<string, string>
default = {
query = 'fallback',
border = 'rounded',
title = vim.fn.has 'nvim-0.9' == 1 and {
{ '', 'TransTitleRound' },
{ ' Trans', 'TransTitle' },
{ '', 'TransTitleRound' },
} or nil, -- need nvim-0.9+
auto_play = true,
---@type {open: string | boolean, close: string | boolean, interval: integer} Hover Window Animation
animation = {
open = 'slid', -- 'fold', 'slid'
close = 'slid',
interval = 12,
},
timeout = 2000,
},
animation = {
open = 'fold',
close = 'fold',
interval = 10,
---@class TransHoverOpts : TransFrontendOpts
hover = {
---@type integer Max Width of Hover Window
width = 37,
---@type integer Max Height of Hover Window
height = 27,
---@type string -- see: /lua/Trans/style/spinner
spinner = 'dots',
---@type string
fallback_message = '{{notfound}} 翻译超时或没有找到相关的翻译',
auto_resize = true,
split_width = 60,
padding = 10, -- padding for hover window width
keymaps = {
pageup = '[[',
pagedown = ']]',
pin = '<leader>[',
close = '<leader>]',
toggle_entry = '<leader>;',
-- play = '_', -- Deprecated
},
---@type string[] auto close events
auto_close_events = {
'InsertEnter',
'CursorMoved',
'BufLeave',
},
---@type table<string, string[]> order to display translate result
order = {
default = {
'str',
'translation',
'definition',
},
offline = {
'title',
'tag',
'pos',
'exchange',
'translation',
'definition',
},
youdao = {
'title',
'translation',
'definition',
'web',
},
},
icon = {
-- or use emoji
list = '●', -- ● | ○ | ◉ | ◯ | ◇ | ◆ | ▪ | ▫ | ⬤ | 🟢 | 🟡 | 🟣 | 🟤 | 🟠| 🟦 | 🟨 | 🟧 | 🟥 | 🟪 | 🟫 | 🟩 | 🟦
star = '', -- ⭐ | ✴ | ✳ | ✲ | ✱ | ✰ | ★ | ☆ | 🌟 | 🌠 | 🌙 | 🌛 | 🌜 | 🌟 | 🌠 | 🌌 | 🌙 |
notfound = ' ', --❔ | ❓ | ❗ | ❕|
yes = '✔', -- ✅ | ✔️ | ☑
no = '', -- ❌ | ❎ | ✖ | ✘ | ✗ |
cell = '■', -- ■ | □ | ▇ | ▏ ▎ ▍ ▌ ▋ ▊ ▉
web = '󰖟', --🌍 | 🌎 | 🌏 | 🌐 |
tag = '',
pos = '',
exchange = '',
definition = '󰗊',
translation = '󰊿',
},
},
tag = {
wait = '#519aba',
fail = '#e46876',
success = '#10b981',
},
engine = {
'本地',
}
},
order = { -- only work on hover mode
'title',
'tag',
'pos',
'exchange',
'translation',
'definition',
},
icon = {
star = '',
notfound = ' ',
yes = '✔',
no = '',
-- --- char: ■ | □ | ▇ | ▏ ▎ ▍ ▌ ▋ ▊ ▉ █
-- --- ◖■■■■■■■◗▫◻ ▆ ▆ ▇⃞ ▉⃞
cell = '■',
-- star = '⭐',
-- notfound = '❔',
-- yes = '✔️',
-- no = '❌'
},
theme = 'default',
-- theme = 'dracula',
-- theme = 'tokyonight',
db_path = '$HOME/.vim/dict/ultimate.db',
engine = {
-- baidu = {
-- appid = '',
-- appPasswd = '',
-- },
-- -- youdao = {
-- appkey = '',
-- appPasswd = '',
-- },
},
-- TODO :
-- register word
-- history = {
-- -- TOOD
-- }
-- TODO :add online translate engine
}
```
## 快捷键绑定
@ -362,10 +356,9 @@ require'Trans'.setup {
> 示例中展示, 将`mm`映射成快捷键
```lua
vim.keymap.set('n', 'mi', '<Cmd>TranslateInput<CR>')
vim.keymap.set({'n', 'x'}, 'mm', '<Cmd>Translate<CR>')
vim.keymap.set({'n', 'x'}, 'mk', '<Cmd>TransPlay<CR>') -- 自动发音选中或者光标下的单词
vim.keymap.set('n', 'mi', '<Cmd>TranslateInput<CR>')
```
## 高亮组
@ -374,50 +367,58 @@ vim.keymap.set('n', 'mi', '<Cmd>TranslateInput<CR>')
```lua
{
TransWord = {
fg = '#7ee787',
bold = true,
},
TransPhonetic = {
link = 'Linenr'
},
TransTitle = {
fg = '#0f0f15',
bg = '#75beff',
bold = true,
},
TransTitleRound = {
fg = '#75beff',
},
TransTag = {
fg = '#e5c07b',
},
TransExchange = {
link = 'TransTag',
},
TransPos = {
link = 'TransTag',
},
TransTranslation = {
link = 'TransWord',
},
TransDefinition = {
link = 'Moremsg',
},
TransWin = {
link = 'Normal',
},
TransBorder = {
link = 'FloatBorder',
},
TransCollins = {
fg = '#faf743',
bold = true,
},
TransFailed = {
fg = '#7aa89f',
},
}
TransWord = {
fg = '#7ee787',
bold = true,
},
TransPhonetic = {
link = 'Linenr'
},
TransTitle = {
fg = '#0f0f15',
bg = '#75beff',
bold = true,
},
TransTitleRound = {
fg = '#75beff',
},
TransTag = {
-- fg = '#e5c07b',
link = '@tag'
},
TransExchange = {
link = 'TransTag',
},
TransPos = {
link = 'TransTag',
},
TransTranslation = {
link = 'TransWord',
},
TransDefinition = {
link = 'Moremsg',
},
TransWin = {
link = 'Normal',
},
TransBorder = {
fg = '#89B4FA',
},
TransCollins = {
fg = '#faf743',
bold = true,
},
TransFailed = {
fg = '#7aa89f',
},
TransWaitting = {
link = 'MoreMsg'
},
TransWeb = {
-- TODO :
link = 'MoreMsg',
}
}
```
## 声明
@ -437,11 +438,16 @@ vim.keymap.set('n', 'mi', '<Cmd>TranslateInput<CR>')
## 待办 (画大饼)
- [x] 多风格样式查询
- [x] 重新录制屏幕截图示例
- [x] 快捷键定义
- [x] 自动读音
- [x] 在线多引擎异步查询
- [x] `句子翻译` | `中翻英` 的支持
- [ ] 迁移文档
- [ ] 多风格样式查询
- [ ] 重新录制屏幕截图示例
- [ ] 变量命名的支持
- [ ] 历史查询结果保存
- [ ] 在线多引擎异步查询
- [ ] `句子翻译` | `中翻英` 的支持
## 项目情况
[![Star History Chart](https://api.star-history.com/svg?repos=JuanZoran/Trans.nvim&type=Date)](https://star-history.com/#JuanZoran/Trans.nvim&Date)

435
doc/Trans.txt Normal file
View File

@ -0,0 +1,435 @@
*Trans.txt* For NVIM v0.8.0 Last change: 2023 March 17
==============================================================================
Table of Contents *Trans-table-of-contents*
1. Trans.nvim |Trans-trans.nvim|
- 特点 |Trans-trans.nvim-特点|
- 屏幕截图 |Trans-trans.nvim-屏幕截图|
- 安装 |Trans-trans.nvim-安装|
- 配置 |Trans-trans.nvim-配置|
- 快捷键绑定 |Trans-trans.nvim-快捷键绑定|
- 高亮组 |Trans-trans.nvim-高亮组|
- 声明 |Trans-trans.nvim-声明|
- 感谢 |Trans-trans.nvim-感谢|
- 贡献 |Trans-trans.nvim-贡献|
- 待办 (画大饼) |Trans-trans.nvim-待办-(画大饼)|
==============================================================================
1. Trans.nvim *Trans-trans.nvim*
- |Trans-trans.nvim|
- |Trans-特点|
- |Trans-屏幕截图|
- |Trans-演示|
- |Trans-主题|
- |Trans-安装|
- |Trans-配置|
- |Trans-快捷键绑定|
- |Trans-高亮组|
- |Trans-声明|
- |Trans-感谢|
- |Trans-贡献|
- |Trans-待办-(画大饼)|
注意: 当前分支目前没有发布, README.MD 的描述并不准确, 遇到问题请切换到 MAIN分支或者联系我 ~
特点 *Trans-trans.nvim-特点*
- 使用纯 lua 编写, 速度极快
`Lazy.nvim`的记录: `➜  Trans.nvim 0.82ms`
- **可以定义快捷键读英文单词**
见 wiki
- 大部分功能可以自定义:
- 高亮
- 悬浮大小
- 排版顺序
- 弹窗大小
- `舒服窗口动画`
- etc (更多可以查看|Trans-配置|)
- **完全离线** 的单词翻译体验 (可能后面会支持在线翻译)
- 支持显示:
- 柯林斯星级
- 牛津 3000 词汇
- 中文翻译
- 英文翻译 (不是英译中, 而是用英文解释)
- 词根
- etc
- 舒服的排版和`动画`
- 支持 `normal`和 `visual`模式 > 不支持 visual-block mode
- 本地词库单词量: `430w`
屏幕截图 *Trans-trans.nvim-屏幕截图*
演示 ~
https://user-images.githubusercontent.com/107862700/213752097-2eee026a-ddee-4531-bf80-ba2cbc8b44ef.mp4
视频演示的在线查询, 查询速度取决于你的网络状况
可以打开音量查看自动读音
https://user-images.githubusercontent.com/107862700/215941500-3293c571-20a1-44e2-b202-77079f158ce9.mp4
主题 ~
如果你有更美观或者更适合的配色, 欢迎提 PR 主题配色在:
`lua/Trans/theme.lua`文件中,你只需要添加你主题的表就可以了
- `default`
- `dracula`
- `tokyonight`
安装 *Trans-trans.nvim-安装*
_安装之前, 首先需要明确本插件的依赖:_
- ECDICT <https://github.com/skywind3000/ECDICT>: 插件所用的离线单词数据库
- sqlite.lua <https://github.com/kkharji/sqlite.lua>: 操作数据库所用的库
- sqlite3: 数据库
Packer.nvim ~
>lua
use {
'JuanZoran/Trans.nvim'
run = function() require('Trans').install() end, -- 自动下载使用的本地词库
requires = 'kkharji/sqlite.lua', ,
-- 如果你不需要任何配置的话, 可以直接按照下面的方式启动
config = function ()
require'Trans'.setup{
-- your configuration here
}
end
}
<
**如果你想要使用 Packer 的惰性加载,这里有一个例子**
>lua
use {
"JuanZoran/Trans.nvim",
keys = {
{ {'n', 'x'}, 'mm' }, -- 换成其他你想用的key即可
{ {'n', 'x'}, 'mk' },
{ 'n', 'mi' },
},
run = function() require('Trans').install() end, -- 自动下载使用的本地词库
requires = { 'kkharji/sqlite.lua', },
config = function()
require("Trans").setup {} -- 启动Trans
vim.keymap.set({"n", 'x'}, "mm", '<Cmd>Translate<CR>', { desc = ' Translate' }) -- 自动判断virtual 还是 normal 模式
vim.keymap.set({'n', 'x'}, 'mk', '<Cmd>TransPlay<CR>', {desc = ' 自动发音'}) -- 自动发音选中或者光标下的单词
end
}
<
Lazy.nvim ~
>lua
{
"JuanZoran/Trans.nvim",
keys = {
-- 可以换成其他你想映射的键
{ 'mm', mode = { 'n', 'x' }, '<Cmd>Translate<CR>', desc = ' Translate' },
{ 'mk', mode = { 'n', 'x' }, '<Cmd>TransPlay<CR>', desc = ' 自动发音' },
-- 目前这个功能的视窗还没有做好可以在配置里将view.i改成hover
{ 'mi', '<Cmd>TranslateInput<CR>', desc = ' Translate From Input' },
},
dependencies = { 'kkharji/sqlite.lua', },
opts = {
-- your configuration there
}
}
<
**注意事项**:
- `install.sh`
- 使用了 `wget`下载词库, 安装请确保你的环境变量中存在 wget
- install.sh 下载后会自动将词库解压, 并移动到 `$HOME/.vim/dict`文件夹下
- 目前仅在 `Ubuntu22.04`的环境下测试通过
> 如果上述条件不符合, 请删掉 `run = 'install.sh'`部分, 考虑手动安装词库
> 如果上述条件满足, 仍出现问题, 欢迎在 issue 里向我反馈,我会及时尝试解决
- 下载词典的过程中, 需要能够 `流畅的访问github下载`
词库文件压缩包大小为: **281M** 解压缩后的大小大概为: 1.2G
- 安装后如果不能正常运行, 请尝试检查一下问题:
- 本机是否已经安装了 `sqlite3`
> Linux 下安装:
> `sudo pacman -S sqlite # Arch`
> `sudo apt-get install sqlite3 libsqlite3-dev # Ubuntu`
**尝试运行 checkhealth Trans**
- **auto_play** 使用步骤:
linux 只需要安装`festival` sudo apt-get install festival festvox-kallpc16k
**如果你想要设置音色,发音可以访问:** Festival 官方
<https://www.cstr.ed.ac.uk/projects/festival/morevoices.html>
可以选择英音、美音、男声、女声
其他操作系统
- 需要确保安装了`nodejs`
- 进入插件的`tts`目录运行`npm install`
> 如果`install`运行正常则自动安装,如果安装失败,请尝试手动安装
- `title`的配置,只对`neovim 0.9+`版本有效
Festival配置(仅针对linux用户) ~
- 配置文件
- 全局配置: `/usr/share/festival/siteinit.scm`
- 用户配置: `~/.festivalrc`
- 更改声音
- 在 festival 的 voices 文件内建立自己的文件夹
一般其默认配置目录在`/usr/share/festival/voices`
示例:
`sudo mkdir /usr/share/festival/voices/my_voices`
- 下载想要的 voices 文件并解压
可能需要 
- 试听在这里 <https://www.cstr.ed.ac.uk/projects/festival/morevoices.html>)
- 下载在这里 <http://festvox.org/packed/festival/2.5/voices/>)
> 假设下载的文件在`Downloads`文件夹, 下载的文件为:`festvox_cmu_us_aew_cg.tar.gz`
示例:
`cd ~/Downloads && tar -xf festvox_cmu_us_aew_cg.tar.gz`
- 将音频文件拷贝到 festival 文件夹 示例:
`sudo cp -r festival/lib/voices/us/cmu_us_aew_cg/
/usr/share/festival/voices/my_voices/`
- 在配置文件中设置默认的声音 示例:
加入`(set! voice_default voice_cmu_indic_hin_ab_cg)`到配置文件
- 安装完成
- 相关说明网站 > 可能需要 
- wiki <https://archlinux.org/packages/community/any/festival-us/> 查看更多详细配置
- 官方网站 <http://festvox.org/dbs/index.html>
- 用户手册 <http://www.festvox.org/docs/manual-2.4.0/festival_toc.html>
配置 *Trans-trans.nvim-配置*
>lua
require'Trans'.setup {
---@type string the directory for database file and password file
dir = os.getenv('HOME') .. '/.vim/dict',
query = 'fallback',
-- backend_order = {},
---@type 'default' | 'dracula' | 'tokyonight' global Trans theme [see lua/Trans/style/theme.lua]
theme = 'default', -- default | tokyonight | dracula
strategy = {
---@type { frontend:string, backend:string | string[] } fallback strategy for mode
default = {
frontend = 'hover',
backend = '*',
},
},
---@type table frontend options
frontend = {
---@class TransFrontendOpts
---@field keymaps table<string, string>
default = {
---@type boolean Whether to auto play the audio
auto_play = true,
border = 'rounded',
title = title, -- need nvim-0.9
---@type {open: string | boolean, close: string | boolean, interval: integer} Hover Window Animation
animation = {
open = 'slid', -- 'fold', 'slid'
close = 'slid',
interval = 12,
},
timeout = 2000,
},
---@class TransHoverOpts : TransFrontendOpts
hover = {
---@type integer Max Width of Hover Window
width = 37,
---@type integer Max Height of Hover Window
height = 27,
---@type string -- see: /lua/Trans/style/spinner
spinner = 'dots',
---@type string -- TODO :support replace with {{special word}}
fallback_message = '{{notfound}} 翻译超时或没有找到相关的翻译',
auto_resize = true,
-- strict = false, -- TODO :No Width limit when str is a sentence
padding = 10, -- padding for hover window width
keymaps = {
pageup = '[[',
pagedown = ']]',
pin = '<leader>[',
close = '<leader>]',
toggle_entry = '<leader>;',
-- play = '_', -- Deprecated
},
---@type string[] auto close events
auto_close_events = {
'InsertEnter',
'CursorMoved',
'BufLeave',
},
---@type table<string, string[]> order to display translate result
order = {
default = {
'str',
'translation',
'definition',
},
offline = {
'title',
'tag',
'pos',
'exchange',
'translation',
'definition',
},
youdao = {
'title',
'translation',
'definition',
'web',
}
},
---@type table<string, string>
icon = {
-- or use emoji
list = '●', -- ● | ○ | ◉ | ◯ | ◇ | ◆ | ▪ | ▫ | ⬤ | 🟢 | 🟡 | 🟣 | 🟤 | 🟦 | 🟨 | 🟧 | 🟥 | 🟪 | 🟫 | 🟩 | 🟠 | 🟦 | 🟨 | 🟧 | 🟥 | 🟪 | 🟫 | 🟩 | 🟠
star = '', -- ⭐ | ✴ | ✳ | ✲ | ✱ | ✰ | ★ | ☆ | 🌟 | 🌠 | 🌙 | 🌛 | 🌜 | 🌟 | 🌠 | 🌌 | 🌙 |
notfound = ' ', --❔ | ❓ | ❗ | ❕|
yes = '✔', -- ✅ | ✔️ | ☑
no = '', -- ❌ | ❎ | ✖ | ✘ | ✗ |
cell = '■', -- ■ | □ | ▇ | ▏ ▎ ▍ ▌ ▋ ▊ ▉ █
web = '󰖟', --🌍 | 🌎 | 🌏 | 🌐 |
tag = ' ',
pos = '',
translation = '󰊿',
definition = '󰗊',
exchange = '✳',
},
},
},
}p
<
快捷键绑定 *Trans-trans.nvim-快捷键绑定*
**示例:**
示例中展示, 将`mm`映射成快捷键
>lua
vim.keymap.set({'n', 'x'}, 'mm', '<Cmd>Translate<CR>')
vim.keymap.set({'n', 'x'}, 'mk', '<Cmd>TransPlay<CR>') -- 自动发音选中或者光标下的单词
<
高亮组 *Trans-trans.nvim-高亮组*
默认定义
>lua
{
TransWord = {
fg = '#7ee787',
bold = true,
},
TransPhonetic = {
link = 'Linenr'
},
TransTitle = {
fg = '#0f0f15',
bg = '#75beff',
bold = true,
},
TransTitleRound = {
fg = '#75beff',
},
TransTag = {
-- fg = '#e5c07b',
link = '@tag'
},
TransExchange = {
link = 'TransTag',
},
TransPos = {
link = 'TransTag',
},
TransTranslation = {
link = 'TransWord',
},
TransDefinition = {
link = 'Moremsg',
},
TransWin = {
link = 'Normal',
},
TransBorder = {
fg = '#89B4FA',
},
TransCollins = {
fg = '#faf743',
bold = true,
},
TransFailed = {
fg = '#7aa89f',
},
TransWaitting = {
link = 'MoreMsg'
},
TransWeb = {
-- TODO :
link = 'MoreMsg',
}
}
<
声明 *Trans-trans.nvim-声明*
- 本插件词典基于ECDICT <https://github.com/skywind3000/ECDICT>
感谢 *Trans-trans.nvim-感谢*
- ECDICT <https://github.com/skywind3000/ECDICT> 本地词典的提供
- sqlite.lua <https://github.com/kharji/sqlite.lua> 数据库访问
- T.vim <https://github.com/sicong-li/T.vim> 灵感来源
贡献 *Trans-trans.nvim-贡献*
更新比较频繁, 文档先鸽了 如果你想要参加这个项目,
可以提 issue, 我会把文档补齐
待办 (画大饼) *Trans-trans.nvim-待办-(画大饼)*
- ☒ 快捷键定义
- ☒ 自动读音
- ☒ 在线多引擎异步查询
- ☒ `句子翻译` | `中翻英` 的支持
- ☐ 多风格样式查询
- ☐ 重新录制屏幕截图示例
- ☐ 变量命名的支持
- ☐ 历史查询结果保存
==============================================================================
2. Links *Trans-links*
1. *default*: ./theme/default.png
2. *dracula*: ./theme/dracula.png
3. *tokyonight*: ./theme/tokyonight.png
Generated by panvimdoc <https://github.com/kdheepak/panvimdoc>
vim:tw=78:ts=8:noet:ft=help:norl:

View File

@ -1,18 +0,0 @@
#!/usr/bin/env bash
set -e
if test -e "$HOME/.vim/dict/ultimate.db"; then
exit
fi
mkdir -p "$HOME/.vim/dict"
wget https://github.com/skywind3000/ECDICT-ultimate/releases/download/1.0.0/ecdict-ultimate-sqlite.zip -O /tmp/dict.zip
unzip /tmp/dict.zip -d "$HOME/.vim/dict" && rm -rf /tmp/dict.zip
uNames=$(uname -s)
osName=${uNames:0:4}
if [ "$osName" != "Linux" ]; then
cd ./tts/ && npm install
fi

View File

@ -1,13 +0,0 @@
{
"$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json",
"Lua.diagnostics.disable": [
"empty-block",
"trailing-space"
],
"Lua.diagnostics.globals": [
"vim",
"user_conf",
"default_conf"
],
"Lua.workspace.checkThirdParty": false
}

5
lua/Trans/.clocignore Normal file
View File

@ -0,0 +1,5 @@
./README.md
./util/md5.lua
./util/base64.lua
./test
./style

18
lua/Trans/README.md Normal file
View File

@ -0,0 +1,18 @@
# TODO
<!--toc:start-->
- [TODO](#todo)
<!--toc:end-->
- [x] Refactor query engine to 'Backend' and 'Frontend'
- [x] Use `Trans.install` instead of `install.sh`
- [x] waitting animation
- [x] init frontend window
- [x] build frontend window format logic
- [x] Add Query FallBack
- [ ] Check if str is a word
- [ ] Unlimit width for sentence
- default_strategy can't deal with table correctly

View File

@ -0,0 +1,73 @@
---@class Baidu: TransOnlineBackend
---@field uri string api uri
---@field salt string
---@field app_id string
---@field app_passwd string
---@field disable boolean
local M = {
uri = 'https://fanyi-api.baidu.com/api/trans/vip/translate',
salt = tostring(math.random(bit.lshift(1, 15))),
name = 'baidu',
name_zh = '百度',
method = 'get',
}
local Trans = require 'Trans'
---@class BaiduQuery
---@field q string
---@field from string
---@field to string
---@field appid string
---@field salt string
---@field sign string
---Get content for query
---@param data TransData
---@return BaiduQuery
function M.get_query(data)
local tmp = M.app_id .. data.str .. M.salt .. M.app_passwd
local sign = Trans.util.md5.sumhexa(tmp)
return {
q = data.str,
from = data.from,
to = data.to,
appid = M.app_id,
salt = M.salt,
sign = sign,
}
end
---@overload fun(body: table, data:TransData): TransResult
---Query Using Baidu API
---@param body table BaiduQuery Response
---@return table|false
function M.formatter(body, data)
local result = body.trans_result
if not result then return false end
-- TEST :whether multi result
assert(#result == 1)
result = result[1]
return {
str = result.src,
[data.from == 'en' and 'translation' or 'definition'] = { result.dst },
}
end
---@class TransBackend
---@field baidu Baidu
return M
-- -- NOTE :free tts:
-- -- https://zj.v.api.aa1.cn/api/baidu-01/?msg=我爱你&choose=0&su=100&yd=5
-- -- 选择转音频的人物女生1 输入0 女生2输入5男生1 输入1男生2 输入2男生3 输入3
-- {
-- body = '{"from":"en","to":"zh","trans_result":[{"src":"require","dst":"\\u8981\\u6c42"}]}',
-- exit = 0,
-- headers = { "Content-Type: application/json", "Date: Thu, 09 Mar 2023 14:01:09 GMT", 'P3p: CP=" OTI DSP COR IVA OUR IND COM "', "Server: Apache", "Set-Cookie: BAIDUID=CB6D99CCD3B5F5278B5BE9428F002FC3:FG=1; expires=Fri, 08-Mar-24 14:01:09 GMT; max-age=31536000; path=/; domain=.baidu.com; version=1", "Tracecode: 00696104432377504778030922", "Content-Length: 79", "", "" },
-- status = 200
-- }

View File

@ -0,0 +1,46 @@
---@class iCiba: TransOnlineBackend
local M = {
uri = 'https://dict-mobile.iciba.com/interface/index.php',
name = 'iciba',
}
---@class iCibaQuery
---@field q string
---@field from string
---@field to string
---@field appid string
---@field salt string
---@field sign string
function M.get_query(data)
return {
word = data.str,
is_need_mean = '1',
m = 'getsuggest',
c = 'word',
}
end
function M.formatter(body, data)
print 'TODO'
-- if true and not status or not body or body.errorCode ~= "0" then
-- data.result.iciba = false
-- data[#data + 1] = res
-- return
-- end
end
-- {
-- message = { {
-- key = "测试",
-- means = { {
-- means = { "test", "testing", "checkout", "measurement " },
-- part = ""
-- } },
-- paraphrase = "test;testing;measurement ;checkout",
-- value = 0
-- } },
-- status = 1
-- }
return M

View File

@ -0,0 +1,181 @@
local Trans = require 'Trans'
local db = require 'sqlite.db'
local path = Trans.conf.dir .. Trans.separator .. 'ultimate.db'
local dict = db:open(path)
local db_name = 'stardict'
vim.api.nvim_create_autocmd('VimLeavePre', {
callback = function()
if db:isopen() then db:close() end
end,
})
---@class TransOfflineBackend
local M = {
name = 'offline',
name_zh = '本地',
no_wait = true,
}
---@param data any
---@return any
---@overload fun(TransData): TransResult
function M.query(data)
if data.is_word == false or data.from == 'zh' then
return
end
local res = dict:select(db_name, {
where = { word = data.str },
keys = M.query_field,
limit = 1,
})[1]
data.result.offline = res and M.formatter(res) or false
end
-- this is a awesome plugin
M.query_field = {
'word',
'phonetic',
'definition',
'translation',
'pos',
'collins',
'oxford',
'tag',
'exchange',
}
local function exist(str)
return str and str ~= ''
end
---@type (fun(res):any)[]
local formatter = {
title = function(res)
local title = {
word = res.word,
oxford = res.oxford,
collins = res.collins,
phonetic = res.phonetic,
}
res.word = nil
res.oxford = nil
res.collins = nil
res.phonetic = nil
return title
end,
tag = function(res)
if not exist(res.tag) then
return
end
local tag_map = {
zk = '中考',
gk = '高考',
ky = '考研',
gre = 'gre ',
cet4 = '四级',
cet6 = '六级',
ielts = '雅思',
toefl = '托福',
}
local tag = {}
for i, _tag in ipairs(vim.split(res.tag, ' ', { plain = true })) do
tag[i] = tag_map[_tag]
end
return tag
end,
exchange = function(res)
if not exist(res.exchange) then
return
end
local exchange_map = {
['0'] = '原型 ',
['1'] = '类别 ',
['p'] = '过去式 ',
['r'] = '比较级 ',
['t'] = '最高级 ',
['s'] = '复数 ',
['d'] = '过去分词 ',
['i'] = '现在分词 ',
['3'] = '第三人称单数',
['f'] = '第三人称单数',
}
local exchange = {}
for _, _exchange in ipairs(vim.split(res.exchange, '/', { plain = true })) do
exchange[exchange_map[_exchange:sub(1, 1)]] = _exchange:sub(3)
end
return exchange
end,
pos = function(res)
if not exist(res.pos) then
return
end
local pos_map = {
a = '代词pron ',
c = '连接词conj ',
i = '介词prep ',
j = '形容词adj ',
m = '数词num ',
n = '名词n ',
p = '代词pron ',
r = '副词adv ',
u = '感叹词int ',
v = '动词v ',
x = '否定标记not ',
t = '不定式标记infm ',
d = '限定词determiner ',
}
local pos = {}
for _, _pos in ipairs(vim.split(res.pos, '/', { plain = true })) do
pos[pos_map[_pos:sub(1, 1)]] = ('%2s%%'):format(_pos:sub(3))
end
return pos
end,
translation = function(res)
if not exist(res.translation) then
return
end
local translation = {}
for i, _translation in ipairs(vim.split(res.translation, '\n', { plain = true })) do
translation[i] = _translation
end
return translation
end,
definition = function(res)
if not exist(res.definition) then
return
end
local definition = {}
for i, _definition in ipairs(vim.split(res.definition, '\n', { plain = true })) do
-- -- TODO :判断是否需要分割空格
definition[i] = _definition:gsub('^%s+', '', 1)
end
return definition
end,
}
---Formater for TransResul
---@param res TransResult
---@return TransResult
function M.formatter(res)
for field, func in pairs(formatter) do
res[field] = func(res)
end
return res
end
---@class TransBackends
---@field offline TransOfflineBackend
return M

View File

@ -0,0 +1,232 @@
---@class Youdao: TransOnlineBackend
---@field uri string api uri
---@field salt string
---@field app_id string
---@field app_passwd string
---@field disable boolean
local M = {
uri = 'https://openapi.youdao.com/api',
salt = tostring(math.random(bit.lshift(1, 15))),
name = 'youdao',
name_zh = '有道',
method = 'get',
}
---@class YoudaoQuery
---@field q string
---@field from string
---@field to string
---@field appid string
---@field salt string
---@field sign string
---Get content for query
---@param data TransData
---@return YoudaoQuery
function M.get_query(data)
local str = data.str
local app_id = M.app_id
local salt = M.salt
local curtime = tostring(os.time())
local chars = vim.str_utf_pos(str)
local count = #chars
local input = count <= 20 and str or
str:sub(1, chars[11] - 1) .. #chars .. str:sub(chars[count - 9])
-- sign=sha256(应用ID+input+salt+curtime+应用密钥) 一二三四五六七八九十
local hash = app_id .. input .. salt .. curtime .. M.app_passwd
local sign = vim.fn.sha256(hash)
return {
q = str,
to = data.from == 'zh' and 'en' or 'zh-CHS',
from = 'auto',
signType = 'v3',
appKey = app_id,
salt = M.salt,
curtime = curtime,
sign = sign,
}
end
local function check_untracked_field(body)
local field = {
'phonetic',
'usPhonetic',
'ukPhonetic',
'text', -- text 短语
'explain', -- String Array 词义解释列表
'wordFormats', -- Object Array 单词形式变化列表
'name', -- String 形式名称,例如:复数
'phrase', -- String 词组
'meaning', -- String 含义
'synonyms', -- JSONObject 近义词
'pos', -- String 词性
'words', -- String Array 近义词列表
'trans', -- String 释义
'antonyms', -- ObjectArray 反义词
'relatedWords', -- JSONArray 相关词
'wordNet', -- JSONObject 汉语词典网络释义
'phonetic', -- String 发音
'meanings', -- ObjectArray 释义
'meaning', -- String 释义
'example', -- array 示例
'sentenceSample', -- text 例句
'sentence', -- text 例句
'sentenceBold', -- text 将查询内容加粗的例句
'wfs', -- text 单词形式变化
'exam_type', -- text 考试类型
}
for _, f in ipairs(field) do
if body[f] then
print(('%s found : %s'):format(f, vim.inspect(body[f])))
end
end
end
function M.debug(body)
if not body then
vim.notify('Unknown errors, nil body', vim.log.levels.ERROR)
end
local debug_msg = ({
[101] = '缺少必填的参数,首先确保必填参数齐全,然后确认参数书写是否正确。',
[102] = '不支持的语言类型',
[103] = '翻译文本过长',
[104] = '不支持的API类型',
[105] = '不支持的签名类型',
[106] = '不支持的响应类型',
[107] = '不支持的传输加密类型',
[108] = '应用ID无效注册账号登录后台创建应用和实例并完成绑定可获得应用ID和应用密钥等信息',
[109] = 'batchLog格式不正确',
[110] = '无相关服务的有效实例,应用没有绑定服务实例可以新建服务实例绑定服务实例。注某些服务的翻译结果发音需要tts实例需要在控制台创建语音合成实例绑定应用后方能使用。',
[111] = '开发者账号无效',
[113] = 'q不能为空',
[120] = '不是词,或未收录',
[201] = '解密失败可能为DES,BASE64,URLDecode的错误',
[202] = '签名检验失败',
[203] = '访问IP地址不在可访问IP列表',
[205] = '请求的接口与应用的平台类型不一致确保接入方式Android SDK、IOS SDK、API与创建的应用平台类型一致。如有疑问请参考入门指南',
[206] = '因为时间戳无效导致签名校验失败',
[207] = '重放请求',
[301] = '辞典查询失败',
[302] = '翻译查询失败',
[303] = '服务端的其它异常',
[305] = '批量翻译部分成功',
[401] = '账户已经欠费,请进行账户充值',
[411] = '访问频率受限,请稍后访问',
[412] = '长请求过于频繁,请稍后访问',
[390001] = '词典名称不正确',
})[tonumber(body.errorCode)]
vim.notify('Youdao API Error: ' .. (debug_msg or vim.inspect(body)), vim.log.levels.ERROR)
end
---@overload fun(TransData): TransResult
---Query Using Youdao API
---@param body table Youdao ouput
---@param data TransData Data obj
---@return table|false?
function M.formatter(body, data)
if body.errorCode ~= '0' then return false end
check_untracked_field(body)
if not body.isWord then
return {
title = body.query,
[data.from == 'en' and 'translation' or 'definition'] = body.translation,
}
end
return {
title = {
word = body.query,
phonetic = body.basic.phonetic,
},
web = body.web,
explains = body.basic.explains,
[data.from == 'en' and 'translation' or 'definition'] = body.translation,
}
end
---@class TransBackend
---@field youdao Youdao
return M
-- INFO :Query Result Example
-- {
-- basic = {
-- explains = { "normal", "regular", "normality" },
-- phonetic = "zhèng cháng"
-- },
-- dict = {
-- url = "yddict://m.youdao.com/dict?le=eng&q=%E6%AD%A3%E5%B8%B8"
-- },
-- errorCode = "0",
-- isWord = true,
-- l = "zh-CHS2en",
-- mTerminalDict = {
-- url = "https://m.youdao.com/m/result?lang=zh-CHS&word=%E6%AD%A3%E5%B8%B8"
-- },
-- query = "正常",
-- requestId = "a8a40c0e-0d3b-49d5-a8fe-b1cd211ff5db",
-- returnPhrase = { "正常" },
-- speakUrl = "https://openapi.youdao.com/ttsapi?q=%E6%AD%A3%E5%B8%B8&langType=zh-CHS&sign=164F6EFF2EFFC7626FB70DBCF796AE70&salt=1678931501049&voice=4&format=mp3&appKey=1858465a8708c121&ttsVoiceStrict=false",
-- tSpeakUrl = "https://openapi.youdao.com/ttsapi?q=normal&langType=en-USA&sign=6A0CF2EF076EA8D82453956B33F69A51&salt=1678931501049&voice=4&format=mp3&appKey=1858465a8708c121&ttsVoiceStrict=false",
-- translation = { "normal" },
-- web = { {
-- key = "正常",
-- value = { "normal", "ordinary", "normo", "regular" }
-- }, {
-- key = "正常利润",
-- value = { "normal profits" }
-- }, {
-- key = "邦交正常化",
-- value = { "normalize relations", "normalization of diplomatic relations" }
-- } },
-- webdict = {
-- url = "http://mobile.youdao.com/dict?le=eng&q=%E6%AD%A3%E5%B8%B8"
-- }
-- }
-- {
-- basic = {
-- explains = { "normal profit" }
-- },
-- dict = {
-- url = "yddict://m.youdao.com/dict?le=eng&q=%E6%AD%A3%E5%B8%B8%E5%88%A9%E6%B6%A6"
-- },
-- errorCode = "0",
-- isWord = true,
-- l = "zh-CHS2en",
-- mTerminalDict = {
-- url = "https://m.youdao.com/m/result?lang=zh-CHS&word=%E6%AD%A3%E5%B8%B8%E5%88%A9%E6%B6%A6"
-- },
-- query = "正常利润",
-- requestId = "87a0b1bf-a5a2-46d1-8604-cd765cd06a90",
-- returnPhrase = { "正常利润" },
-- speakUrl = "https://openapi.youdao.com/ttsapi?q=%E6%AD%A3%E5%B8%B8%E5%88%A9%E6%B6%A6&langType=zh-CHS&sign=5DC3A57D7D4CB21892D0D77E6968F03D&salt=1678950274137&voice=4&format=mp3&appKey=1858465a8708c121&ttsVoiceStrict=false",
-- tSpeakUrl = "https://openapi.youdao.com/ttsapi?q=Normal+profit&langType=en-USA&sign=325FA5994D5D3B859DF21E3522577AFB&salt=1678950274137&voice=4&format=mp3&appKey=1858465a8708c121&ttsVoiceStrict=false",
-- translation = { "Normal profit" },
-- web = { {
-- key = "正常利润",
-- value = { "normal profits" }
-- }, {
-- key = "非正常利润",
-- value = { "abnormal profits" }
-- }, {
-- key = "正常利润率",
-- value = { "normal profit rate" }
-- } },
-- webdict = {
-- url = "http://mobile.youdao.com/dict?le=eng&q=%E6%AD%A3%E5%B8%B8%E5%88%A9%E6%B6%A6"
-- }
-- }

View File

@ -1,165 +0,0 @@
local api, fn = vim.api, vim.fn
---@class buf
---@field bufnr integer buffer handle
---@field size integer buffer line count
local buffer = {}
---Clear all content in buffer
function buffer:wipe()
api.nvim_buf_set_lines(self.bufnr, 0, -1, false, {})
self.size = 0
end
---delete buffer [_start, _end] line content [one index]
---@param _start integer start line index
---@param _end integer end line index
function buffer:del(_start, _end)
if not _start then
fn.deletebufline(self.bufnr, '$')
else
_end = _end or _start
fn.deletebufline(self.bufnr, _start, _end)
end
self.size = api.nvim_buf_line_count(self.bufnr)
end
---Set buffer option
---@param name string option name
---@param value any option value
function buffer:set(name, value)
api.nvim_buf_set_option(self.bufnr, name, value)
end
---get buffer option
---@param name string option name
---@return any
function buffer:option(name)
return api.nvim_buf_get_option(self.bufnr, name)
end
function buffer:delete()
api.nvim_buf_delete(self.bufnr, { force = true })
end
---Set buffer load keymap
---@param key string
---@param operation function | string
function buffer:map(key, operation)
vim.keymap.set('n', key, operation, {
buffer = self.bufnr,
silent = true,
})
end
---Execute normal keycode in this buffer[no recursive]
---@param key string key code
function buffer:normal(key)
api.nvim_buf_call(self.bufnr, function()
vim.cmd([[normal! ]] .. key)
end)
end
---@return boolean
---@nodiscard
function buffer:is_valid()
return api.nvim_buf_is_valid(self.bufnr)
end
---Get buffer [i, j] line content
---@param i integer? start line index
---@param j integer? end line index
---@return string[]
function buffer:lines(i, j)
i = i and i - 1 or 0
j = j and j - 1 or -1
return api.nvim_buf_get_lines(self.bufnr, i, j, false)
end
---Calculate buffer content display height
---@param width integer
---@return integer height
function buffer:height(width)
local size = self.size
local lines = self:lines()
local height = 0
for i = 1, size do
height = height + math.max(1, (math.ceil(lines[i]:width() / width)))
end
return height
end
---Add|Set line content
---@param nodes string|table|table[] string -> as line content | table -> as a node | table[] -> as node[]
---@param index number? line number should be set[one index]
function buffer:addline(nodes, index)
local newsize = self.size + 1
assert(index == nil or index <= newsize)
index = index or newsize
if index == newsize then
self.size = newsize
end
if type(nodes) == 'string' then
self[index] = nodes
return
end
local line = index - 1
local bufnr = self.bufnr
local col = 0
if type(nodes[1]) == 'string' then
self[index] = nodes[1]
nodes:load(bufnr, line, col)
return
end
local strs = {}
local num = #nodes
for i = 1, num do
strs[i] = nodes[i][1]
end
self[index] = table.concat(strs)
for i = 1, num do
local node = nodes[i]
node:load(bufnr, line, col)
col = col + #node[1]
end
end
function buffer:init()
self.bufnr = api.nvim_create_buf(false, false)
self:set('filetype', 'Trans')
self:set('buftype', 'nofile')
self.size = 0
end
---@private
buffer.__index = function(self, key)
local res = buffer[key]
if res then
return res
elseif type(key) == 'number' then
return fn.getbufoneline(self.bufnr, key)
else
error('invalid key' .. key)
end
end
---@private
buffer.__newindex = function(self, key, text)
assert(key <= self.size + 1)
fn.setbufline(self.bufnr, key, text)
end
---buffer constructor
---@return buf
return function()
return setmetatable({
bufnr = -1,
size = 0,
}, buffer)
end

View File

@ -0,0 +1,61 @@
local Trans = require 'Trans'
---@class TransBackend
---@field no_wait? boolean whether need to wait for the result
---@field all_name string[] @all backend name
---@field name string @backend name
---@field name_zh string @backend name in Chinese
---@class TransOnlineBackend: TransBackend
---@field uri string @request uri
---@field method 'get' | 'post' @request method
---@field formatter fun(body: table, data: TransData): TransResult|false|nil @formatter
---@field get_query fun(data: TransData): table<string, string> @get query
---@field header? table<string, string> | fun(data: TransData): table<string, string> @request header
---@field debug? fun(body: table?) @debug
local conf = Trans.conf
--- INFO :Parse online engine keys config file
local path = conf.dir .. '/Trans.json'
local file = io.open(path, 'r')
local user_conf = {}
if file then
local content = file:read '*a'
user_conf = vim.json.decode(content) or user_conf
file:close()
end
local all_name = {}
for _, config in ipairs(user_conf) do
if not config.disable then
all_name[#all_name + 1] = config.name
user_conf[config.name] = config
end
end
---@class TransBackends
---@field all_name string[] all backend names
---@class Trans
---@field backend TransBackends
return setmetatable({
all_name = all_name,
}, {
__index = function(self, name)
---@type TransBackend
local backend = require('Trans.backend.' .. name)
for key, value in pairs(user_conf[name] or {}) do
backend[key] = value
end
self[name] = backend
return backend
end,
})

216
lua/Trans/core/buffer.lua Normal file
View File

@ -0,0 +1,216 @@
local api, fn = vim.api, vim.fn
---@class TransBuffer
---@field bufnr integer buffer handle
---@field [number] string|TransNode|TransNode[] buffer[line] content
local buffer = {}
-- INFO : corountine can't invoke C function
---Clear all content in buffer
function buffer:wipe()
api.nvim_buf_set_lines(self.bufnr, 0, -1, false, {})
end
---Delete buffer [_start, _end] line content [one index]
---@param _start? integer start line index
---@param _end? integer end line index
function buffer:deleteline(_start, _end)
---@diagnostic disable-next-line: cast-local-type
_start = _start and _start - 1 or self:line_count() - 1
_end = _end or _start + 1 -- because of end exclusive
api.nvim_buf_set_lines(self.bufnr, _start, _end, false, {})
end
---Set buffer option
---@param name string option name
---@param value any option value
function buffer:set(name, value)
api.nvim_buf_set_option(self.bufnr, name, value)
end
---Get buffer option
---@param name string option name
---@return any
function buffer:option(name)
return api.nvim_buf_get_option(self.bufnr, name)
end
---Destory buffer
function buffer:destroy()
api.nvim_buf_delete(self.bufnr, { force = true })
end
---Set buffer load keymap
---@param key string
---@param operation function | string
function buffer:map(key, operation)
vim.keymap.set('n', key, operation, {
buffer = self.bufnr,
silent = true,
})
end
---Execute keycode in normal this buffer[no recursive]
---@param key string key code
function buffer:normal(key)
api.nvim_buf_call(self.bufnr, function()
vim.cmd([[normal! ]] .. key)
end)
end
---@nodiscard
---@return boolean
function buffer:is_valid()
return api.nvim_buf_is_valid(self.bufnr)
end
---Get buffer [i, j] line content
---@param i integer? start line index
---@param j integer? end line index
---@return string[]
function buffer:lines(i, j)
i = i and i - 1 or 0
j = j or -1 -- because of end exclusive
return api.nvim_buf_get_lines(self.bufnr, i, j, false)
end
---Add highlight to buffer
---@param linenr number line number should be set[one index]
---@param hl_group string highlight group
---@param col_start? number column start [zero index]
---@param col_end? number column end
---@param ns number? highlight namespace
function buffer:add_highlight(linenr, hl_group, col_start, col_end, ns)
-- vim.print(linenr, hl_group, col_start, col_end, ns)
linenr = linenr - 1
col_start = col_start or 0
api.nvim_buf_add_highlight(self.bufnr, ns or -1, hl_group, linenr, col_start, col_end or -1)
end
---Get buffer line count
---@return integer
function buffer:line_count()
local line_count = api.nvim_buf_line_count(self.bufnr)
if line_count == 1 and self[1] == '' then
return 0
end
return line_count
end
---Set line content
---@param nodes string|table|table[] string -> as line content | table -> as a node | table[] -> as node[]
---@param one_index number? line number should be set[one index] or let it be nil to append
function buffer:setline(nodes, one_index)
local append_line_index = self:line_count() + 1
one_index = one_index or append_line_index
if one_index > append_line_index then
for i = append_line_index, one_index - 1 do
self:setline('', i)
end
end
if type(nodes) == 'string' then
fn.setbufline(self.bufnr, one_index, nodes)
return
end
if type(nodes[1]) == 'string' then
---@diagnostic disable-next-line: assign-type-mismatch, param-type-mismatch
fn.setbufline(self.bufnr, one_index, nodes[1])
nodes:render(self, one_index, 0)
return
end
local strs = {}
local num = #nodes
for i = 1, num do
strs[i] = nodes[i][1]
end
fn.setbufline(self.bufnr, one_index, table.concat(strs))
local col = 0
for i = 1, num do
local node = nodes[i]
node:render(self, one_index, col)
col = col + #node[1]
end
end
buffer.__index = function(self, key)
local res = buffer[key]
if res then
return res
elseif type(key) == 'number' then
-- return fn.getbufoneline(self.bufnr, key) -- Vimscript Function Or Lua API ?? -- INFO :only work on neovim-nightly
return api.nvim_buf_get_lines(self.bufnr, key - 1, key, true)[1]
else
error('invalid key: ' .. key)
end
end
buffer.__newindex = function(self, key, nodes)
if type(key) == 'number' then
self:setline(nodes, key)
else
rawset(self, key, nodes)
end
end
---Init buffer with bufnr
---@param bufnr? integer buffer handle
function buffer:init(bufnr)
self.bufnr = bufnr or api.nvim_create_buf(false, false)
self:set('filetype', 'Trans')
self:set('buftype', 'nofile')
end
---@nodiscard
---TransBuffer constructor
---@param bufnr? integer buffer handle
---@return TransBuffer
function buffer.new(bufnr)
local new_buf = setmetatable({}, buffer)
new_buf:init(bufnr)
return new_buf
end
--- HACK :available options:
--- - id
--- - end_row
--- - end_col
--- - hl_eol
--- - virt_text
--- - virt_text_pos
--- - virt_text_win_col
--- - hl_mode
--- - virt_lines
--- - virt_lines_above
--- - virt_lines_leftcol
--- - ephemeral
--- - right_gravity
--- - end_right_gravity
--- - priority
--- - strict
--- - sign_text
--- - sign_hl_group
--- - number_hl_group
--- - line_hl_group
--- - cursorline_hl_group
--- - conceal
--- - ui_watched
---Add Extmark to buffer
---@param ns number highlight namespace
---@param linenr number line number should be set[one index]
---@param col_start number column start
function buffer:set_extmark(ns, linenr, col_start, opts)
linenr = linenr and linenr - 1 or -1
return api.nvim_buf_set_extmark(self.bufnr, ns, linenr, col_start, opts)
end
---@class Trans
---@field buffer TransBuffer
return buffer

138
lua/Trans/core/conf.lua Normal file
View File

@ -0,0 +1,138 @@
---@class Trans
---@field conf TransConf
---@class TransConf
return {
---@type string the directory for database file and password file
dir = require 'Trans'.plugin_dir,
warning = true,
debug = true,
---@type 'default' | 'dracula' | 'tokyonight' global Trans theme [see lua/Trans/style/theme.lua]
theme = 'default', -- default | tokyonight | dracula
strategy = {
---@type { frontend:string, backend:string | string[] } fallback strategy for mode
default = {
frontend = 'hover',
backend = '*',
},
},
---@type table frontend options
frontend = {
---@class TransFrontendOpts
---@field keymaps table<string, string>
default = {
query = 'fallback',
border = 'rounded',
title = vim.fn.has 'nvim-0.9' == 1 and {
{ '', 'TransTitleRound' },
{ ' Trans', 'TransTitle' },
{ '', 'TransTitleRound' },
} or nil, -- need nvim-0.9+
auto_play = true,
---@type {open: string | boolean, close: string | boolean, interval: integer} Hover Window Animation
animation = {
open = 'slid', -- 'fold', 'slid'
close = 'slid',
interval = 12,
},
timeout = 2000,
},
---@class TransHoverOpts : TransFrontendOpts
hover = {
---@type integer Max Width of Hover Window
width = 37,
---@type integer Max Height of Hover Window
height = 27,
---@type string -- see: /lua/Trans/style/spinner
spinner = 'dots',
---@type string
fallback_message = '{{notfound}} 翻译超时或没有找到相关的翻译',
auto_resize = true,
split_width = 60,
padding = 10, -- padding for hover window width
keymaps = {
pageup = '[[',
pagedown = ']]',
pin = '<leader>[',
close = '<leader>]',
toggle_entry = '<leader>;',
-- play = '_', -- Deprecated
},
---@type string[] auto close events
auto_close_events = {
'InsertEnter',
'CursorMoved',
'BufLeave',
},
---@type table<string, string[]> order to display translate result
order = {
default = {
'str',
'translation',
'definition',
},
offline = {
'title',
'tag',
'pos',
'exchange',
'translation',
'definition',
},
youdao = {
'title',
'translation',
'definition',
'web',
},
},
icon = {
-- or use emoji
list = '', -- ● | ○ | ◉ | ◯ | ◇ | ◆ | ▪ | ▫ | ⬤ | 🟢 | 🟡 | 🟣 | 🟤 | 🟠| 🟦 | 🟨 | 🟧 | 🟥 | 🟪 | 🟫 | 🟩 | 🟦
star = '', -- ⭐ | ✴ | ✳ | ✲ | ✱ | ✰ | ★ | ☆ | 🌟 | 🌠 | 🌙 | 🌛 | 🌜 | 🌟 | 🌠 | 🌌 | 🌙 |
notfound = '', --❔ | ❓ | ❗ | ❕|
yes = '', -- ✅ | ✔️ | ☑
no = '', -- ❌ | ❎ | ✖ | ✘ | ✗ |
cell = '', -- ■ | □ | ▇ | ▏ ▎ ▍ ▌ ▋ ▊ ▉
web = '󰖟', --🌍 | 🌎 | 🌏 | 🌐 |
tag = '',
pos = '',
exchange = '',
definition = '󰗊',
translation = '󰊿',
},
},
},
}
-- TODO :
-- float = {
-- width = 0.8,
-- height = 0.8,
-- border = 'rounded',
-- keymap = {
-- quit = 'q',
-- },
-- animation = {
-- open = 'fold',
-- close = 'fold',
-- interval = 10,
-- },
-- tag = {
-- wait = '#519aba',
-- fail = '#e46876',
-- success = '#10b981',
-- },
-- },
-- local title = {
-- "████████╗██████╗ █████╗ ███╗ ██╗███████╗",
-- "╚══██╔══╝██╔══██╗██╔══██╗████╗ ██║██╔════╝",
-- " ██║ ██████╔╝███████║██╔██╗ ██║███████╗",
-- " ██║ ██╔══██╗██╔══██║██║╚██╗██║╚════██║",
-- " ██║ ██║ ██║██║ ██║██║ ╚████║███████║",
-- " ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝",
--}

90
lua/Trans/core/curl.lua Normal file
View File

@ -0,0 +1,90 @@
---@class TransCurl
local curl = {}
---@class RequestResult
---@field body string
---@field exit integer exit code
---@field error string error message from stderr
---@class TransCurlOptions
---@field query table<string, string> query arguments
---@field output string output file path
---@field headers table<string, string> headers
---@field callback fun(result: RequestResult)
---@async
---Send a GET request use curl
---@param uri string uri for request
---@param opts
---| { query?: table<string, string>, output?: string, headers?: table<string, string>, callback: fun(result: RequestResult), extra?: string[] }
function curl.get(uri, opts)
local query = opts.query
local output = opts.output
local headers = opts.headers
local callback = opts.callback
local extra = opts.extra
-- INFO :Init Curl command with {s}ilent and {G}et
local cmd = vim.list_extend({ 'curl', '-GLs', uri }, extra or {})
local size = #cmd
local function add(value)
size = size + 1
cmd[size] = value
end
-- INFO :Add headers
if headers then
for k, v in pairs(headers) do
add(('-H %q: %q'):format(k, v))
end
end
-- INFO :Add arguments
if query then
for k, v in pairs(query) do
add(('--data-urlencode %q=%q'):format(k, v))
end
end
-- INFO :Store output to file
if output then
add(('-o %q'):format(output))
end
-- INFO : Start a job
local outputs = {}
local on_stdout = function(_, stdout)
local str = table.concat(stdout)
if str ~= '' then
outputs[#outputs + 1] = str
end
end
local on_exit = function(_, exit)
callback {
exit = exit,
body = table.concat(outputs),
}
end
-- vim.print(table.concat(cmd, ' '))
vim.fn.jobstart(table.concat(cmd, ' '), {
stdin = 'null',
on_stdout = on_stdout,
on_exit = on_exit,
})
end
--- TODO :
-- curl.post = function ()
--
-- end
---@class Trans
---@field curl TransCurl
return curl

83
lua/Trans/core/data.lua Normal file
View File

@ -0,0 +1,83 @@
local Trans = require 'Trans'
---@class TransData
---@field from string @Source language type
---@field to string @Target language type
---@field is_word boolean @Is the str a word
---@field str string @The original string
---@field mode string @The mode of the str
---@field result table<string, TransResult|nil|false> @The result of the translation
---@field frontend TransFrontend
---@field trace table<string, string> debug message
---@field backends table<string, TransBackend>
local M = {}
M.__index = M
---TransData constructor
---@param opts table
---@return TransData
function M.new(opts)
local mode = opts.mode
local str = opts.str
local strategy = Trans.conf.strategy[mode]
local data = setmetatable({
str = str,
mode = mode,
result = {},
trace = {},
}, M)
data.frontend = Trans.frontend[strategy.frontend].new()
data.backends = {}
for i, name in ipairs(strategy.backend) do
data.backends[i] = Trans.backend[name]
end
if Trans.util.is_English(str) then
data.from = 'en'
data.to = 'zh'
else
data.from = 'zh'
data.to = 'en'
end
data.is_word = Trans.util.is_word(str)
return data
end
---@class TransResult
---@field str? string? @The original string
---@field title table | string @table: {word, phonetic, oxford, collins}
---@field tag string[]? @array of tags
---@field pos table<string, string>? @table: {name, value}
---@field exchange table<string, string>? @table: {name, value}
---@field definition? string[]? @array of definitions
---@field translation? string[]? @array of translations
---@field web? table<string, string[]>[]? @web definitions
---@field explains? string[]? @basic explains
---Get the first available result [return nil if no result]
---@return TransResult | false?
---@return string? backend.name
function M:get_available_result()
local result = self.result
if result['offline'] then return result['offline'], 'offline' end
for _, backend in ipairs(self.backends) do
if result[backend.name] then
---@diagnostic disable-next-line: return-type-mismatch
return result[backend.name], backend.name
end
end
end
---@class Trans
---@field data TransData
return M

View File

@ -0,0 +1,32 @@
local Trans = require 'Trans'
local conf = Trans.conf
local frontend_opts = conf.frontend
---@class TransFrontend
---@field opts TransFrontendOpts
---@field get_active_instance fun():TransFrontend?
---@field process fun(self: TransFrontend, data: TransData)
---@field wait fun(self: TransFrontend): fun(backend: TransBackend): boolean Update wait status
---@field execute fun(action: string) @Execute action for frontend instance
---@field fallback fun() @Fallback method when no result
---@field setup? fun() @Setup method for frontend [optional] **NOTE: This method will be called when frontend is first used**
---@class Trans
---@field frontend TransFrontend
return setmetatable({}, {
__index = function(self, name)
local opts = vim.tbl_extend('keep', frontend_opts[name] or {}, frontend_opts.default)
---@type TransFrontend
local frontend = require('Trans.frontend.' .. name)
frontend.opts = opts
self[name] = frontend
if frontend.setup then
frontend.setup()
end
return frontend
end,
})

View File

@ -0,0 +1,75 @@
local function trans()
local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
local paragraphs = {}
-- TODO : trim empty lines in the beginning and the end
for index, line in ipairs(lines) do
if line:match '%S+' then
table.insert(paragraphs, { index - 1, line })
end
end
local Trans = require 'Trans'
local baidu = Trans.backend.baidu
---@cast baidu Baidu
for _, line in ipairs(paragraphs) do
local query = baidu.get_query {
str = line[2],
from = 'en',
to = 'zh',
}
Trans.curl.get(baidu.uri, {
query = query,
callback = function(output)
-- vim.print(output)
local body = output.body
local status, ret = pcall(vim.json.decode, body)
assert(status and ret, 'Failed to parse json:' .. vim.inspect(body))
local result = ret.trans_result
assert(result, 'Failed to get result: ' .. vim.inspect(ret))
result = result[1]
line.translation = result.dst
end,
})
end
local ns = vim.api.nvim_create_namespace 'Trans'
for _, line in ipairs(paragraphs) do
local index = line[1]
local co = coroutine.running()
local times = 0
while not line.translation do
vim.defer_fn(function()
coroutine.resume(co)
end, 100)
print('waitting' .. ('.'):rep(times))
times = times + 1
-- if times == 10 then break end
coroutine.yield()
end
local translation = line.translation
print(translation, index)
Trans.util.main_loop(function()
vim.api.nvim_buf_set_extmark(0, ns, index, #line[2], {
virt_lines = {
{ { translation, 'MoreMsg' } },
},
})
end)
print 'done'
end
-- TODO :双语翻译
end
return function()
coroutine.wrap(trans)()
end

View File

@ -0,0 +1,57 @@
---@class Trans
---@field install fun() Download database and tts dependencies
return function()
local Trans = require 'Trans'
local fn = vim.fn
-- INFO :Check ultimate.db exists
local dir = Trans.conf.dir
local path = dir .. 'ultimate.db'
if fn.isdirectory(dir) == 0 then
fn.mkdir(dir, 'p')
end
if fn.filereadable(path) == 1 then
vim.notify('Database already exists', vim.log.WARN)
return
end
-- INFO :Download ultimate.db
local uri = 'https://github.com/skywind3000/ECDICT-ultimate/releases/download/1.0.0/ecdict-ultimate-sqlite.zip'
local zip = dir .. 'ultimate.zip'
local continue = fn.filereadable(zip) == 1
local handle = function(output)
if output.exit == 0 and fn.filereadable(zip) then
if fn.executable 'unzip' == 0 then
vim.notify('unzip not found, Please unzip ' .. zip .. 'manually', vim.log.ERROR)
return
end
local cmd = string.format('unzip %s -d %s', zip, dir)
local status = os.execute(cmd)
os.remove(zip)
if status == 0 then
vim.notify('Download database successfully', vim.log.INFO)
return
end
end
local debug_message = 'Download database failed:' .. vim.inspect(output)
vim.notify(debug_message, vim.log.ERROR)
end
Trans.curl.get(uri, {
output = zip,
callback = handle,
extra = continue and { '-C', '-' } or nil,
})
local message = continue and 'Continue download database' or 'Begin to download database'
vim.notify(message, vim.log.levels.INFO)
-- INFO : Install tts dependencies
if fn.has 'linux' == 0 and fn.has 'mac' == 0 then
os.execute 'cd ./tts && npm install'
end
end

63
lua/Trans/core/setup.lua Normal file
View File

@ -0,0 +1,63 @@
local Trans = require 'Trans'
local function set_strategy_opts(conf)
local all_backends = Trans.backend.all_name
local g_strategy = conf.strategy
local function parse_backend(backend)
if type(backend) == 'string' then
return backend == '*' and all_backends or { backend }
end
return backend
end
local default_strategy = g_strategy.default
default_strategy.backend = parse_backend(default_strategy.backend)
default_strategy.__index = default_strategy
g_strategy.default = nil
setmetatable(g_strategy, {
__index = function()
return default_strategy
end,
})
for _, strategy in pairs(g_strategy) do
strategy.backend = parse_backend(strategy.backend)
setmetatable(strategy, default_strategy)
end
end
local function define_highlights(conf)
local set_hl = vim.api.nvim_set_hl
local highlights = Trans.style.theme[conf.theme]
for hl, opt in pairs(highlights) do
set_hl(0, hl, opt)
end
end
---@class Trans
---@field setup fun(opts: { mode: string, mode: string })
return function(opts)
if opts then
Trans.conf = vim.tbl_deep_extend('force', Trans.conf, opts)
end
local conf = Trans.conf
conf.dir = vim.fn.expand(conf.dir)
set_strategy_opts(conf)
define_highlights(conf)
if Trans.conf.warning then
vim.notify([[
新版本v2已经发布, 见:
https://github.com/JuanZoran/Trans.nvim
请使用当前默认(v2)分支
]], vim.log.levels.WARN)
end
end

View File

@ -0,0 +1,104 @@
local Trans = require 'Trans'
local util = Trans.util
local function init_opts(opts)
opts = opts or {}
opts.mode = opts.mode or vim.fn.mode()
opts.str = util.get_str(opts.mode)
return opts
end
---To Do Online Query
---@param data TransData @data
---@param backend TransOnlineBackend @backend
local function do_query(data, backend)
-- TODO : template method for online query
local name = backend.name
local uri = backend.uri
local method = backend.method
local formatter = backend.formatter
local query = backend.get_query(data)
local header = type(backend.header) == 'function' and backend.header(data) or backend.header
local function handle(output)
local status, body = pcall(vim.json.decode, output.body)
if not status or not body then
if not Trans.conf.debug then
backend.debug(body)
data.trace[name] = output
end
data.result[name] = false
return
end
-- vim.print(data.result[name])
data.result[name] = formatter(body, data)
end
Trans.curl[method](uri, {
query = query,
callback = handle,
header = header,
})
-- Hook ?
end
---@type table<string, fun(data: TransData):boolean>
local strategy = {
fallback = function(data)
local result = data.result
Trans.backend.offline.query(data)
if result.offline then return true end
local update = data.frontend:wait()
for _, backend in ipairs(data.backends) do
do_query(data, backend)
local name = backend.name
---@cast backend TransBackend
while result[name] == nil do
if not update(backend) then break end
end
if result[name] then return true end
end
return false
end,
--- TODO :More Strategys
}
-- HACK : Core process logic
local function process(opts)
opts = init_opts(opts)
local str = opts.str
if not str or str == '' then return end
-- Find in cache
if Trans.cache[str] then
local data = Trans.cache[str]
data.frontend:process(data)
return
end
local data = Trans.data.new(opts)
if strategy[data.frontend.opts.query](data) then
Trans.cache[data.str] = data
data.frontend:process(data)
else
data.frontend:fallback()
end
end
---@class Trans
---@field translate fun(opts: { frontend: string?, mode: string?}?) Translate string core function
return function(opts)
coroutine.wrap(process)(opts)
end

209
lua/Trans/core/util.lua Normal file
View File

@ -0,0 +1,209 @@
local fn, api = vim.fn, vim.api
---@class TransUtil
local M = require 'Trans'.metatable 'util'
---Get selected text
---@return string
function M.get_select()
local _start = fn.getpos 'v'
local _end = fn.getpos '.'
if _start[2] > _end[2] or (_start[3] > _end[3] and _start[2] == _end[2]) then
_start, _end = _end, _start
end
local s_row, s_col = _start[2], _start[3]
local e_row, e_col = _end[2], _end[3]
-- print(s_row, e_row, s_col, e_col)
---@type string
---@diagnostic disable-next-line: assign-type-mismatch
local line = fn.getline(e_row)
local uidx = vim.str_utfindex(line, math.min(#line, e_col))
---@diagnostic disable-next-line: param-type-mismatch
e_col = vim.str_byteindex(line, uidx)
if s_row == e_row then
return line:sub(s_col, e_col)
else
local lines = fn.getline(s_row, e_row)
local e = #lines
lines[1] = lines[1]:sub(s_col)
lines[e] = line:sub(1, e_col)
return table.concat(lines)
end
end
---Get Text which need to be translated
---@param mode string
---@return string
function M.get_str(mode)
return ({
n = function()
return fn.expand '<cword>'
end,
v = function()
api.nvim_input '<Esc>'
return M.get_select()
end,
i = function()
return fn.input '需要翻译的字符串: '
end,
V = function()
print 'TODO'
end,
})[mode]():match '^%s*(.-)%s*$'
end
---Puase coroutine for {ms} milliseconds
---@param ms integer
function M.pause(ms)
local co = coroutine.running()
vim.defer_fn(function()
coroutine.resume(co)
end, ms)
coroutine.yield()
end
---Detect whether the string is English
---@param str string
---@return boolean
function M.is_English(str)
local char = { str:byte(1, -1) }
for i = 1, #str do
if char[i] > 128 then
return false
end
end
return true
end
---Calculates the height of the text to be displayed
---@param lines string[] text to be displayed
---@param width integer width of the window
---@return integer height display height
function M.display_height(lines, width)
local height = 0
for _, line in ipairs(lines) do
height = height + math.max(1, (math.ceil(line:width() / width)))
end
return height
end
---Calculates the width of the text to be displayed
---@param lines string[] text to be displayed
---@return integer width display width
function M.display_width(lines)
local width = 0
for _, line in ipairs(lines) do
width = math.max(line:width(), width)
end
return width
end
---Center node utility function
---@param node string -- TODO :Node
---@param win_width integer window width
---@return string
function M.center(node, win_width)
if type(node) == 'string' then
local space = math.max(0, math.floor((win_width - node:width()) / 2))
return string.rep(' ', space) .. node
end
local str = node[1]
win_width = str:width()
local space = math.max(0, math.floor((win_width - str:width()) / 2))
node[1] = string.rep(' ', space) .. str
return node
end
---Execute function in main loop
---@param func function function to be executed
function M.main_loop(func)
local co = coroutine.running()
vim.defer_fn(function()
func()
coroutine.resume(co)
end, 0)
coroutine.yield()
end
---Split text into paragraphs
---@param lines string[] text to be split
---@return string[][] paragraphs
function M.split_to_paragraphs(lines, opts)
--- TODO :More options and better algorithm to detect paragraphs
opts = opts or {}
local paragraphs = {}
local paragraph = {}
for _, line in ipairs(lines) do
if line == '' then
paragraphs[#paragraphs + 1] = paragraph
paragraph = {}
else
paragraph[#paragraph + 1] = line
end
end
return paragraphs
end
---Get visible lines in the window or current window
---@param opts { winid: integer, height: integer }?
---@return string[]
function M.visible_lines(opts)
opts = opts or {}
-- TODO : Use getpos('w0') and getpos('w$') to get the visible lines
-- INFO : don't calculate the height of statusline and cmdheight or winbar?
local winid = opts.winid or 0
local win_height = opts.height or api.nvim_win_get_height(winid)
local current_line = api.nvim_win_get_cursor(winid)[1]
local current_relative_line = vim.fn.winline()
local _start = current_line - current_relative_line
local _end = _start + win_height - vim.o.cmdheight --[[ - 1 -- maybe 1 for statusline?? ]]
return api.nvim_buf_get_lines(0, _start, _end, false)
end
---Detect whether the string is a word
---@param str string
---@return boolean
function M.is_word(str)
return str:match '%w+' == str
end
---@param list any[]
---@param step table
---@return any[]
function M.list_concat(list, step)
local size = #list
local ret = { list[1] }
if size <= 1 then return ret end
for i = 2, size do
ret[i * 2 - 2] = step
ret[i * 2 - 1] = list[i]
end
-- FIXME : Use deepcopy step?
return ret
end
---Get the field of the list
---@param list any[]
---@param field any
---@return any[]
function M.list_fields(list, field)
local ret = {}
for i, v in ipairs(list) do
ret[i] = v[field]
end
return ret
end
---@class Trans
---@field util TransUtil
return M

215
lua/Trans/core/window.lua Normal file
View File

@ -0,0 +1,215 @@
local api = vim.api
---@class Trans
local Trans = require 'Trans'
---@class TransWindow
local window = {}
---Change window attached buffer
---@param buffer TransBuffer
function window:set_buf(buffer)
api.nvim_win_set_buf(self.winid, buffer.bufnr)
self.buffer = buffer
end
---Check window valid
---@return boolean
function window:is_valid()
return api.nvim_win_is_valid(self.winid)
end
---Set window option
---@param name string option name
---@param value any
function window:set(name, value)
if self:is_valid() then
api.nvim_win_set_option(self.winid, name, value)
end
end
---@param name string option name
---@return any
function window:option(name)
return api.nvim_win_get_option(self.winid, name)
end
---@param height integer
function window:set_height(height)
api.nvim_win_set_height(self.winid, height)
end
---@param width integer
function window:set_width(width)
api.nvim_win_set_width(self.winid, width)
end
---Get window width
---@return integer
function window:width()
return api.nvim_win_get_width(self.winid)
end
---Get window height
---@return integer
function window:height()
return api.nvim_win_get_height(self.winid)
end
---Auto adjust window size
---@param height? integer max height
function window:adjust_height(height)
local display_height = Trans.util.display_height(self.buffer:lines(), self:width())
height = height and math.min(display_height, height) or display_height
self:smooth_expand {
field = 'height',
to = height,
}
end
---Expand window [width | height] value
---@param opts
---|{ field: 'width'|'height', to: integer}
function window:smooth_expand(opts)
local field = opts.field -- width | height
local from = api['nvim_win_get_' .. field](self.winid)
local to = opts.to
if from == to then return end
local pause = Trans.util.pause
local method = api['nvim_win_set_' .. field]
local wrap = self:option 'wrap'
self:set('wrap', false)
local interval = self.animation.interval or 12
for i = from + 1, to, (from < to and 1 or -1) do
method(self.winid, i)
pause(interval)
end
self:set('wrap', wrap)
end
---Resize window
---@param opts
---|{ width: integer, height: integer }
function window:resize(opts)
if opts.height and self:height() ~= opts.height then
self:smooth_expand {
field = 'height',
to = opts.height,
}
end
if opts.width and self:width() ~= opts.width then
self:smooth_expand {
field = 'width',
to = opts.width,
}
end
end
---Try to close window with animation?
function window:try_close()
local close_animation = self.animation.close
if close_animation then
local field = ({
slid = 'width',
fold = 'height',
})[close_animation]
self:smooth_expand {
field = field,
to = 1,
}
end
api.nvim_win_close(self.winid, true)
end
---Set window local highlight group
---@param name string
---@param opts table highlight config
---@param ns integer namespace
function window:set_hl(name, opts, ns)
api.nvim_set_hl(ns, name, opts)
end
---Open window with animation?
function window:open()
local win_opts = self.win_opts
local open_animation = self.animation.open
if open_animation then
local field = ({
slid = 'width',
fold = 'height',
})[open_animation]
local to = win_opts[field]
win_opts[field] = 1
self.winid = api.nvim_open_win(self.buffer.bufnr, self.enter, win_opts)
self:smooth_expand {
field = field,
to = to,
}
else
self.winid = api.nvim_open_win(self.buffer.bufnr, self.enter, win_opts)
end
end
window.__index = window
---@alias WindowOpts
---|{style: string, border: string, focusable: boolean, noautocmd?: boolean, relative: string, width: integer, height: integer, col: integer, row: integer, zindex?: integer, title?: table | string}
---@class TransWindowOpts
local default_opts = {
enter = false,
winid = -1,
---@type WindowOpts
win_opts = {
style = 'minimal',
border = 'rounded',
focusable = false,
noautocmd = true,
},
}
---@class TransWindow
---@field buffer TransBuffer attached buffer object
---@field win_opts table window config [**When open**]
---@field winid integer window handle
---@field enter boolean cursor should [enter] window when open
---@field animation
---|{open: string | boolean, close: string | boolean, interval: integer} Hover Window Animation
-- local win_opt = {
-- zindex = zindex,
-- width = width,
-- height = height,
-- col = col,
-- row = row,
-- border = border,
-- title = title,
-- relative = relative,
-- }
---Create new window
---@param opts TransWindowOpts window config
---@return TransWindow
function window.new(opts)
opts = vim.tbl_deep_extend('keep', opts, default_opts)
local win = setmetatable(opts, window)
---@cast win TransWindow
win:open()
return win
end
---@class Trans
---@field window TransWindow
return window

View File

@ -0,0 +1,123 @@
local Trans = require 'Trans'
---@class TransFloat
local M = {}
function M.new()
-- TODO :
end
return M
-- local function set_tag_hl(name, status)
-- local hl = conf.float.tag[status]
-- m_window:set_hl(name, {
-- fg = '#000000',
-- bg = hl,
-- })
-- m_window:set_hl(name .. 'round', {
-- fg = hl,
-- })
-- end
-- local function set_title()
-- local title = m_window:new_content()
-- local github = ' https://github.com/JuanZoran/Trans.nvim'
-- title:addline(
-- title:center(it(github, '@text.uri'))
-- )
-- local f = '%s(%d)'
-- local tags = {}
-- local load_tag = function(engine, index)
-- set_tag_hl(engine, 'wait')
-- local round = engine .. 'round'
-- table.insert(tags, t(
-- it('', round),
-- it(f:format(engine_map[engine], index), engine),
-- it('', round)
-- ))
-- end
-- load_tag('offline', 1)
-- title:addline(unpack(tags))
-- title:newline('')
-- end
-- local action = {
-- quit = function()
-- -- m_window:try_close()
-- end,
-- }
-- local exist = function(str)
-- return str and str ~= ''
-- end
-- local function process()
-- TODO :
-- local icon = conf.icon
-- m_content:addline(m_content:format {
-- nodes = {
-- it(m_result.word, 'TransWord'),
-- t(
-- it('['),
-- it(exist(m_result.phonetic) and m_result.phonetic or icon.notfound, 'TransPhonetic'),
-- it(']')
-- ),
-- it(m_result.collins and icon.star:rep(m_result.collins) or icon.notfound, 'TransCollins'),
-- it(m_result.oxford == 1 and icon.yes or icon.no)
-- },
-- width = math.floor(m_window.width * 0.5)
-- })
-- m_content:addline(it('该窗口还属于实验性功能 .... '))
-- end
-- return function(word)
-- buffer:init()
-- -- TODO :online query
-- -- local float = conf.float
-- vim.notify([[
-- [注意]:
-- float窗口目前还待开发
-- 如果需要input查询功能, 请将窗口改成hover]])
-- local opt = {
-- relative = 'editor',
-- width = float.width,
-- height = float.height,
-- border = float.border,
-- title = float.title,
-- animation = float.animation,
-- row = bit.rshift((vim.o.lines - float.height), 1),
-- col = bit.rshift((vim.o.columns - float.width), 1),
-- zindex = 20,
-- }
-- m_window = require('Trans.window')(true, opt)
-- set_title()
-- m_content = m_window:new_content()
-- m_result = require('Trans.query.offline')(word)
-- if m_result then
-- set_tag_hl('offline', 'success')
-- process()
-- else
-- set_tag_hl('offline', 'fail')
-- end
-- m_window:open()
-- m_window:bufset('bufhidden', 'wipe')
-- for act, key in pairs(float.keymap) do
-- m_window:map(key, action[act])
-- end
-- end
-- local engine_map = {
-- baidu = '百度',
-- youdao = '有道',
-- iciba = 'iciba',
-- offline = '本地',
-- }

View File

@ -0,0 +1,53 @@
local api = vim.api
---@type table<string, fun(hover: TransHover)>
local strategy = {
pageup = function(hover)
hover.buffer:normal 'gg'
end,
pagedown = function(hover)
hover.buffer:normal 'G'
end,
pin = function(hover)
if hover.pin then
return
end
hover.pin = true
local window = hover.window
local width, height = window:width(), window:height()
local col = vim.o.columns - width - 3
window:try_close()
window = hover:init_window {
col = col,
width = width,
height = height,
relative = 'editor',
}
window:set('wrap', true)
end,
close = function(hover)
hover:destroy()
end,
toggle_entry = function(hover)
if api.nvim_get_current_win() ~= hover.window.winid then
api.nvim_set_current_win(hover.window.winid)
return
end
for _, winid in ipairs(api.nvim_list_wins()) do
if winid ~= hover.window.winid then
api.nvim_set_current_win(winid)
break
end
end
end,
}
---@class TransHover
---@field execute fun(hover: TransHover, action: string)
return function(hover, action)
-- TODO :
strategy[action](hover)
end

View File

@ -0,0 +1,259 @@
---@type Trans
local Trans = require 'Trans'
-- FIXME :Adjust Window Size
---@class TransHover: TransFrontend
---@field ns integer @namespace for hover window
---@field buffer TransBuffer @buffer for hover window
---@field window TransWindow @hover window
---@field queue TransHover[] @hover queue for all hover instances
---@field destroy_funcs fun(hover:TransHover)[] @functions to be executed when hover window is closed
---@field opts TransHoverOpts @options for hover window
---@field pin boolean @whether hover window is pinned
local M = Trans.metatable('frontend.hover', {
ns = vim.api.nvim_create_namespace 'TransHoverWin',
queue = {},
})
M.__index = M
function M.setup()
local set = vim.keymap.set
for action, key in pairs(M.opts.keymaps) do
set('n', key, function()
local instance = M.get_active_instance()
if instance then
coroutine.wrap(instance.execute)(instance, action)
else
return key
end
end)
end
end
---Create a new hover instance
---@return TransHover new_instance
function M.new()
local new_instance = {
pin = false,
buffer = Trans.buffer.new(),
destroy_funcs = {},
}
M.queue[#M.queue + 1] = new_instance
return setmetatable(new_instance, M)
end
---Get the first active instances
---@return TransHover
function M.get_active_instance()
M.clear_dead_instance()
return M.queue[#M.queue]
end
---Clear dead instance
function M.clear_dead_instance()
local queue = M.queue
for i = #queue, 1, -1 do
if not queue[i]:is_available() then
queue[i]:destroy()
table.remove(queue, i)
end
end
end
---Destroy hover instance and execute destroy functions
function M:destroy()
coroutine.wrap(function()
for _, func in ipairs(self.destroy_funcs) do
func(self)
end
if self.window:is_valid() then
self.window:try_close()
end
if self.buffer:is_valid() then
self.buffer:destroy()
end
self.pin = false
end)()
end
---Init hover window
---@param opts?
---|{width?: integer, height?: integer, col?: integer, row?: integer, relative?: string}
---@return unknown
function M:init_window(opts)
opts = opts or {}
local m_opts = self.opts
local option = {
buffer = self.buffer,
animation = m_opts.animation,
win_opts = {
col = opts.col or 1,
row = opts.row or 1,
width = opts.width or m_opts.width,
height = opts.height or m_opts.height,
relative = opts.relative or 'cursor',
title = m_opts.title,
title_pos = m_opts.title and 'center' or nil,
zindex = 100,
},
}
self.window = Trans.window.new(option)
return self.window
end
---Get Formatted icon text
---@param format string format string
---@return string formatted text
---@return integer _ replaced count
function M:icon_format(format)
return format:gsub('{{(%w+)}}', self.opts.icon)
end
---Get Check function for waiting
---@return fun(backend: TransBackend): boolean
function M:wait()
local util = Trans.util
local opts = self.opts
local buffer = self.buffer
local pause = util.pause
local cell = opts.icon.cell
local spinner = Trans.style.spinner[opts.spinner]
local times = opts.width - spinner[1]:width()
local size = #spinner
local interval = math.floor(opts.timeout / opts.width)
self:init_window {
height = 2,
width = opts.width,
}
self.waitting = true
local cur = 0
local pr = util.node.prompt
local it = util.node.item
return function(backend)
cur = cur + 1
buffer[1] = pr(backend.name_zh)
buffer[2] = it { spinner[cur % size + 1] .. (cell):rep(cur), 'TransWaitting' }
pause(interval)
return cur < times
end
end
---FallBack window for no result
function M:fallback()
local opts = self.opts
local fallback_msg = self:icon_format(opts.fallback_message)
local buffer = self.buffer
buffer:wipe()
buffer[1] = Trans.util.center(fallback_msg, opts.width)
buffer:add_highlight(1, 'TransFailed')
if not self.window then
self:init_window {
height = buffer:line_count(),
width = self.opts.width,
}
end
self:defer()
end
---Defer function when process done
function M:defer()
Trans.util.main_loop(function()
self.window:set('wrap', true)
self.buffer:set('modifiable', false)
local auto_close_events = self.opts.auto_close_events
if not auto_close_events then return end
vim.api.nvim_create_autocmd(auto_close_events, {
callback = function(opts)
vim.defer_fn(function()
if not self.pin and vim.api.nvim_get_current_win() ~= self.window.winid then
pcall(vim.api.nvim_del_autocmd, opts.id)
self:destroy()
end
end, 0)
end,
})
end)
end
---Display Result in hover window
---@param data TransData
---@overload fun(result:TransResult)
function M:process(data)
if not self.waitting and self.window and self.window:is_valid() then
self:execute 'toggle_entry'
return
end
self.waitting = false
local result, name = data:get_available_result()
if not result then
self:fallback()
return
end
local util = Trans.util
local opts = self.opts
local buffer = self.buffer
if opts.auto_play then
(data.from == 'en' and data.str or result.definition[1]):play()
end
-- vim.pretty_print(result)
util.main_loop(function()
buffer[buffer:is_valid() and 'wipe' or 'init'](buffer)
---@cast name string
self:load(result, name, opts.order[name])
end)
local window = self.window
local lines = buffer:lines()
local valid = window and window:is_valid()
local width =
valid and
(opts.auto_resize and
math.max(
math.min(opts.width, util.display_width(lines) + opts.padding),
math.min(data.str:width(), opts.split_width)
)
or opts.width)
or math.min(opts.width, util.display_width(lines) + opts.padding)
local height = math.min(opts.height, util.display_height(lines, width))
if valid then
window:resize { width = width, height = height }
else
window = self:init_window {
height = height,
width = width,
}
end
self:defer()
end
---Check if hover window and buffer are valid
---@return boolean @whether hover window and buffer are valid
function M:is_available()
return self.buffer:is_valid() and self.window:is_valid()
end
---@class TransFrontend
---@field hover TransHover @hover frontend
return M

View File

@ -0,0 +1,72 @@
local node = require 'Trans'.util.node
local it, pr = node.item, node.prompt
local interval = (' '):rep(4)
local M = setmetatable({}, {
__call = function(self, hover, result, name, order)
order = order or hover.opts.order.default
local method = self.renderer[name]
for _, field in ipairs(order) do
method[field](hover, result)
end
end,
})
---@alias TransHoverFormatter fun(hover:TransHover, result: TransResult)
---@alias TransHoverRenderer table<string, TransHoverFormatter>
---@type TransHoverRenderer
local default = {
str = function(hover, result)
hover.buffer:setline(it { result.str, 'TransWord' })
end,
translation = function(hover, result)
local translation = result.translation
if not translation then return end
local buffer = hover.buffer
buffer:setline(pr(hover.opts.icon.translation .. ' 中文翻译'))
for _, value in ipairs(translation) do
buffer:setline(
it { interval .. value, 'TransTranslation' }
)
end
buffer:setline ''
end,
definition = function(hover, result)
local definition = result.definition
if not definition then return end
local buffer = hover.buffer
buffer:setline(pr(hover.opts.icon.definition .. ' 英文注释'))
for _, value in ipairs(definition) do
buffer:setline(
it { interval .. value, 'TransDefinition' }
)
end
buffer:setline ''
end,
}
---@diagnostic disable-next-line: assign-type-mismatch
default.__index = default
---@type table<string, TransHoverRenderer>
M.renderer = setmetatable({}, {
__index = function(tbl, key)
local status, method = pcall(require, 'Trans.frontend.hover.' .. key)
tbl[key] = setmetatable(status and method or {}, default)
return tbl[key]
end,
})
---@class TransHover
---@field load fun(hover: TransHover, result: TransResult, name: string, order: string[])
return M

View File

@ -0,0 +1,96 @@
local node = require 'Trans'.util.node
local it, t, f, co = node.item, node.text, node.format, node.prompt
local interval = (' '):rep(4)
---@class TransHover
---@field offline TransHoverRenderer
---@type TransHoverRenderer
local M = {}
function M.title(hover, result)
local title = result.title
if not title then return end
if type(title) == 'string' then
hover.buffer:setline(it { title, 'TransWord' })
return
end
local icon = hover.opts.icon
local word = title.word
local oxford = title.oxford
local collins = title.collins
local phonetic = title.phonetic
hover.buffer:setline(f {
it { word, 'TransWord' },
t {
it { '[' },
it { (phonetic and phonetic ~= '') and phonetic or icon.notfound, 'TransPhonetic' },
it { ']' },
},
it { collins and icon.star:rep(collins) or icon.notfound, 'TransCollins' },
it { oxford == 1 and icon.yes or icon.no },
width = hover.opts.width,
})
end
function M.tag(hover, result)
local tag = result.tag
if not tag then return end
local buffer = hover.buffer
buffer:setline(co(hover.opts.icon.tag .. ' 标签'))
local size = #tag
for i = 1, size, 3 do
buffer:setline(it {
interval .. tag[i] ..
(tag[i + 1] and interval .. tag[i + 1] ..
(tag[i + 2] and interval .. tag[i + 2] or '') or ''),
'TransTag'
})
end
buffer:setline ''
end
function M.exchange(hover, result)
local exchange = result.exchange
if not exchange then return end
local buffer = hover.buffer
buffer:setline(co(hover.opts.icon.exchange .. ' 词形变化'))
for description, value in pairs(exchange) do
buffer:setline(
it { interval .. description .. interval .. value, 'TransExchange' }
)
end
buffer:setline ''
end
function M.pos(hover, result)
local pos = result.pos
if not pos then return end
local buffer = hover.buffer
buffer:setline(co(hover.opts.icon.pos .. ' 词性'))
for description, value in pairs(pos) do
buffer:setline(
it { interval .. description .. interval .. value, 'TransPos' }
)
end
buffer:setline ''
end
return M

View File

@ -0,0 +1,67 @@
local node = require 'Trans'.util.node
local it, t, f, pr = node.item, node.text, node.format, node.prompt
---@type TransHoverRenderer
local M = {}
local interval = (' '):rep(4)
function M.web(hover, result)
if not result.web then return end
local buffer = hover.buffer
buffer:setline(pr(hover.opts.icon.web .. ' 网络释义'))
local function remove_duplicate(strs)
local uniq_strs = {}
local str_map = {}
local opts = { plain = true, trim_empty = true }
for i = 1, #strs do
local fields = vim.split(strs[i], '; ', opts)
for j = 1, #fields do
local field = fields[j]
if not str_map[field] then
uniq_strs[#uniq_strs + 1] = field
str_map[field] = true
end
end
end
return uniq_strs
end
local indent = interval .. ' ' .. hover.opts.icon.list .. ' '
for _, w in ipairs(result.web) do
buffer:setline(it {
interval .. w.key,
'TransWeb'
})
for _, v in ipairs(remove_duplicate(w.value)) do
buffer:setline(it {
indent .. v,
'TransWeb'
})
end
end
buffer:setline ''
end
function M.explains(hover, result)
local explains = result.explains
if not explains then return end
local buffer = hover.buffer
buffer:setline(pr '基本释义')
for i = 1, #explains, 2 do
buffer:setline(it {
interval .. explains[i] ..
(explains[i + 1] and interval .. explains[i + 1] or ''),
'TransExplains'
})
end
buffer:setline ''
end
M.title = require 'Trans'.frontend.hover.offline.title
return M

View File

@ -1,63 +1,99 @@
local M = {}
local Trans = require 'Trans'
local health, fn = vim.health, vim.fn
M.check = function()
local health = vim.health
local ok = health.report_ok
local warn = health.report_warn
local error = health.report_error
local ok = health.report_ok
local warn = health.report_warn
local error = health.report_error
local has = fn.has
local executable = fn.executable
local has = vim.fn.has
local executable = vim.fn.executable
-- INFO :Check neovim version
if has('nvim-0.9') == 1 then
ok [[
[PASS]: you have Trans.nvim with full features in neovim-nightly
]]
local function check_neovim_version()
if has 'nvim-0.9' == 1 then
ok [[You have [neovim-nightly] ]]
else
warn [[
[WARN]: Trans Title requires Neovim 0.9 or newer
See neovim-nightly: https://github.com/neovim/neovim/releases/tag/nightly
]]
end
-- INFO :Check Sqlite
local has_sqlite = pcall(require, 'sqlite')
if has_sqlite then
ok [[
[PASS]: Dependency sqlite.lua is installed
]]
else
error [[
[ERROR]: Dependency sqlite.lua can't work correctly
Please Read the doc in github carefully
]]
end
if executable('sqlite3') then
ok [[
[PASS]: Dependency sqlite3 found
]]
else
error [[
[ERROR]: Dependency sqlite3 not found
]]
end
-- INFO :Check stardict
local db_path = vim.fn.expand(require('Trans').conf.db_path)
if vim.fn.filereadable(db_path) == 1 then
ok [[
[PASS]: Stardict database found
]]
else
error [[
[PASS]: Stardict database not found
Please check the doc in github
warn [[Trans Title requires Neovim 0.9 or newer
See neovim-nightly: [https://github.com/neovim/neovim/releases/tag/nightly]
]]
end
end
return M
local function check_plugin_dependencies()
local plugin_dependencies = {
-- 'plenary',
'sqlite',
}
for _, dep in ipairs(plugin_dependencies) do
if pcall(require, dep) then
ok(string.format('Dependency [%s] is installed', dep))
else
error(string.format('Dependency [%s] is not installed', dep))
end
end
end
local function check_binary_dependencies()
local binary_dependencies = {
'curl',
'sqlite3',
}
if has 'linux' == 1 then
binary_dependencies[3] = 'festival'
elseif has 'mac' == 1 then
binary_dependencies[3] = 'say'
else
binary_dependencies[3] = 'node'
end
for _, dep in ipairs(binary_dependencies) do
if executable(dep) == 1 then
ok(string.format('Binary dependency [%s] is installed', dep))
else
error(string.format('Binary dependency [%s] is not installed', dep))
end
end
end
local function check_database()
local db_path = Trans.conf.dir .. Trans.separator .. 'ultimate.db'
if fn.filereadable(db_path) == 1 then
ok [[ultimate database found ]]
else
error [[Stardict database not found
[Manually]: Please check the doc in github: [https://github.com/JuanZoran/Trans.nvim]
[Automatically]: Try to run `:lua require "Trans".install()`
]]
end
end
local function check_configure_file()
local path = fn.expand(Trans.conf.dir .. Trans.separator .. 'Trans.json')
if not fn.filereadable(path) then
warn 'Backend configuration file[%s] not found'
end
local file = io.open(path, 'r')
local valid = file and pcall(vim.json.decode, file:read '*a')
if valid then
ok(string.format([[Backend configuration file [%s] found and valid ]], path))
else
error(string.format(
[[Backend configuration file [%s] invalid
Please check the doc in github: [https://github.com/JuanZoran/Trans.nvim]
]],
path
))
end
end
local function check()
check_database()
check_neovim_version()
check_configure_file()
check_plugin_dependencies()
check_binary_dependencies()
end
return { check = check }

View File

@ -1,252 +1,45 @@
local M = {}
local api, fn = vim.api, vim.fn
if fn.executable 'sqlite3' ~= 1 then
error 'Please check out sqlite3'
---Set or Get metatable which will find module in folder
---@param folder_name string
---@param origin table? table to be set metatable
---@return table
local function metatable(folder_name, origin)
return setmetatable(origin or {}, {
__index = function(tbl, key)
local status, result = pcall(require, ('Trans.%s.%s'):format(folder_name, key))
if status then
tbl[key] = result
return result
end
end,
})
end
local win_title = fn.has 'nvim-0.9' == 1 and {
{ '', 'TransTitleRound' },
{ ' Trans', 'TransTitle' },
{ '', 'TransTitleRound' },
} or nil
---@class string
---@field width function @Get string display width
---@field play function @Use tts to play string
-- local title = {
-- "████████╗██████╗ █████╗ ███╗ ██╗███████╗",
-- "╚══██╔══╝██╔══██╗██╔══██╗████╗ ██║██╔════╝",
-- " ██║ ██████╔╝███████║██╔██╗ ██║███████╗",
-- " ██║ ██╔══██╗██╔══██║██║╚██╗██║╚════██║",
-- " ██║ ██║ ██║██║ ██║██║ ╚████║███████║",
-- " ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝",
--}
string.width = api.nvim_strwidth
string.isEn = function(self)
local char = { self:byte(1, -1) }
for i = 1, #self do
if char[i] > 128 then
return false
end
end
return true
local sep = vim.loop.os_uname().sysname == 'Windows' and '\\' or '/'
---@class Trans
---@field style table @Style module
---@field cache table<string, TransData> @Cache for translated data object
---@field plugin_dir string @Plugin directory
---@field separator string @Path separator
local M = metatable('core', {
cache = {},
style = metatable 'style',
plugin_dir = debug.getinfo(1, 'S').source:sub(2):match('(.-)lua' .. sep .. 'Trans'),
separator = sep,
})
M.metatable = metatable
---Get abs_path of file
---@param path string[]
---@param is_dir boolean?
---@return string
function M.relative_path(path, is_dir)
return M.plugin_dir .. table.concat(path, sep) .. (is_dir and sep or '')
end
string.play = fn.has 'linux' == 1 and function(self)
local cmd = ([[echo "%s" | festival --tts]]):format(self)
fn.jobstart(cmd)
end or fn.has 'mac' == 1 and function(self)
local cmd = ([[say "%s"]]):format(self)
fn.jobstart(cmd)
end or function(self)
local seperator = fn.has 'unix' and '/' or '\\'
local file = debug.getinfo(1, 'S').source:sub(2):match '(.*)lua' .. seperator .. 'tts' .. seperator .. 'say.js'
fn.jobstart('node ' .. file .. ' ' .. self)
end
M.conf = {
warning = true,
view = {
i = 'float',
n = 'hover',
v = 'hover',
},
hover = {
width = 37,
height = 27,
border = 'rounded',
title = win_title,
keymap = {
pageup = '[[',
pagedown = ']]',
pin = '<leader>[',
close = '<leader>]',
toggle_entry = '<leader>;',
play = '_',
},
animation = {
-- open = 'fold',
-- close = 'fold',
open = 'slid',
close = 'slid',
interval = 12,
},
auto_close_events = {
'InsertEnter',
'CursorMoved',
'BufLeave',
},
auto_play = true,
timeout = 2000,
spinner = 'dots', -- 查看所有样式: /lua/Trans/util/spinner
-- spinner = 'moon'
},
float = {
width = 0.8,
height = 0.8,
border = 'rounded',
title = win_title,
keymap = {
quit = 'q',
},
animation = {
open = 'fold',
close = 'fold',
interval = 10,
},
tag = {
wait = '#519aba',
fail = '#e46876',
success = '#10b981',
},
},
order = { -- only work on hover mode
'title',
'tag',
'pos',
'exchange',
'translation',
'definition',
},
icon = {
star = '',
notfound = '',
yes = '',
no = '',
-- --- char: ■ | □ | ▇ | ▏ ▎ ▍ ▌ ▋ ▊ ▉ █
-- --- ◖■■■■■■■◗▫◻ ▆ ▆ ▇⃞ ▉⃞
cell = '',
-- star = '⭐',
-- notfound = '❔',
-- yes = '✔️',
-- no = '❌'
},
theme = 'default',
-- theme = 'dracula',
-- theme = 'tokyonight',
db_path = '$HOME/.vim/dict/ultimate.db',
engine = {
youdao = {},
-- baidu = {
-- appid = '',
-- appPasswd = '',
-- },
-- -- youdao = {
-- appkey = '',
-- appPasswd = '',
-- },
},
}
M.setup = function(opts)
if opts then
M.conf = vim.tbl_deep_extend('force', M.conf, opts)
end
local conf = M.conf
local float = conf.float
if 0 < float.height and float.height <= 1 then
float.height = math.floor((vim.o.lines - vim.o.cmdheight - 1) * float.height)
end
if 0 < float.width and float.width <= 1 then
float.width = math.floor(vim.o.columns * float.width)
end
local engines = {}
local i = 1
for k, _ in pairs(conf.engine) do
engines[i] = k
i = i + 1
end
conf.engines = engines
local set_hl = api.nvim_set_hl
local hls = require 'Trans.ui.theme'[conf.theme]
for hl, opt in pairs(hls) do
set_hl(0, hl, opt)
end
if M.conf.warning then
vim.notify([[
新版本v2已经发布, 见:
https://github.com/JuanZoran/Trans.nvim
该分支已经不再维护, 请尝试迁移到新分支
]], vim.log.levels.WARN)
end
end
local function get_select()
local _start = fn.getpos 'v'
local _end = fn.getpos '.'
if _start[2] > _end[2] or (_start[3] > _end[3] and _start[2] == _end[2]) then
_start, _end = _end, _start
end
local s_row, s_col = _start[2], _start[3]
local e_row, e_col = _end[2], _end[3]
-- print(s_row, e_row, s_col, e_col)
---@type string
---@diagnostic disable-next-line: assign-type-mismatch
local line = fn.getline(e_row)
local uidx = vim.str_utfindex(line, math.min(#line, e_col))
e_col = vim.str_byteindex(line, uidx)
if s_row == e_row then
return line:sub(s_col, e_col)
else
local lines = fn.getline(s_row, e_row)
local i = #lines
lines[1] = lines[1]:sub(s_col)
lines[i] = line:sub(1, e_col)
return table.concat(lines)
end
end
M.get_word = function(mode)
local word
if mode == 'n' then
word = fn.expand '<cword>'
elseif mode == 'v' then
api.nvim_input '<ESC>'
word = get_select()
elseif mode == 'i' then
-- TODO Use Telescope with fuzzy finder
---@diagnostic disable-next-line: param-type-mismatch
word = fn.input '请输入需要查询的单词:'
else
error('invalid mode: ' .. mode)
end
return word
end
M.translate = function(mode, view)
if M.conf.warning then
vim.notify([[
新版本已经发布, 见:
https://github.com/JuanZoran/Trans.nvim
该分支已经不再维护, 请尝试迁移到新分支
]], vim.log.levels.WARN)
end
vim.validate {
mode = { mode, 's', true },
view = { view, 's', true },
}
mode = mode or api.nvim_get_mode().mode
view = view or M.conf.view[mode]
assert(mode and view)
local word = M.get_word(mode)
if word == nil or word == '' then
return
else
require('Trans.view.' .. view)(word:gsub('^%s+', '', 1))
end
end
M.ns = api.nvim_create_namespace 'Trans'
return M

View File

@ -1,73 +0,0 @@
local api = vim.api
local ns = require('Trans').ns
local add_hl = api.nvim_buf_add_highlight
local item_meta = {
load = function(self, bufnr, line, col)
if self[2] then
add_hl(bufnr, ns, self[2], line, col, col + #self[1])
end
end,
}
local text_meta = {
load = function(self, bufnr, line, col)
local items = self.items
local step = self.step or ''
local len = #step
for i = 1, self.size do
local item = items[i]
item:load(bufnr, line, col)
col = col + #item[1] + len
end
end
}
item_meta.__index = item_meta
text_meta.__index = function(self, key)
local res = text_meta[key]
if res then
return res
elseif key == 1 then
return table.concat(self.strs, self.step)
end
end
return {
item = function(text, highlight)
return setmetatable({
[1] = text,
[2] = highlight,
}, item_meta)
end,
text = function(items)
local strs = {}
local size = #items
assert(size > 1)
for i = 1, size do
strs[i] = items[i][1]
end
return setmetatable({
strs = strs,
size = size,
items = items,
}, text_meta)
end,
format = function(opts)
local text = opts.text
local size = text.size
local width = opts.width
local spin = opts.spin or ' '
local wid = text[1]:width()
local space = math.max(math.floor((width - wid) / (size - 1)), 0)
if space > 0 then
text.step = spin:rep(space)
end
return text
end,
}

View File

@ -1,65 +0,0 @@
local baidu = require('Trans').conf.engine.baidu
local appid = baidu.appid
local appPasswd = baidu.appPasswd
local salt = tostring(math.random(bit.lshift(1, 15)))
local uri = 'https://fanyi-api.baidu.com/api/trans/vip/translate'
if appid == '' or appPasswd == '' then
error('请查看README, 实现在线翻译或者设置将在线翻译设置为false')
end
local post = require('Trans.util.curl').POST
local function get_field(word, isEn)
local to = isEn and 'zh' or 'en'
local tmp = appid .. word .. salt .. appPasswd
local sign = require('Trans.util.md5').sumhexa(tmp)
return {
q = word,
from = 'auto',
to = to,
appid = appid,
salt = salt,
sign = sign,
}
end
---返回一个channel
---@param word string
---@return table
return function(word)
local isEn = word:isEn()
local query = get_field(word, isEn)
local result = {}
post(uri, {
data = query,
headers = {
content_type = "application/x-www-form-urlencoded",
},
callback = function(str)
local ok, res = pcall(vim.json.decode, str)
if ok and res and res.trans_result then
result[1] = {
title = { word = word },
[isEn and 'translation' or 'definition'] = res.trans_result[1].dst,
}
if result.callback then
result.callback(result[1])
end
else
result[1] = false
end
end,
})
return result
end
-- NOTE :free tts:
-- https://zj.v.api.aa1.cn/api/baidu-01/?msg=我爱你&choose=0&su=100&yd=5
-- 选择转音频的人物女生1 输入0 女生2输入5男生1 输入1男生2 输入2男生3 输入3

View File

@ -1,47 +0,0 @@
local _, db = pcall(require, 'sqlite.db')
if not _ then
error('Please check out sqlite.lua')
end
-- INFO : init database
local path = require('Trans').conf.db_path
local dict = db:open(path)
vim.api.nvim_create_autocmd('VimLeavePre', {
once = true,
callback = function()
if db:isopen() then
db:close()
end
end
})
return function(word)
local res = (dict:select('stardict', {
where = { word = word, },
keys = {
'word',
'phonetic',
'definition',
'translation',
'pos',
'collins',
'oxford',
'tag',
'exchange',
},
limit = 1,
}))[1]
if res then
res.title = {
word = res.word,
oxford = res.oxford,
collins = res.collins,
phonetic = res.phonetic,
}
end
return res
end

View File

@ -1,74 +0,0 @@
local GET = require("Trans.util.curl").GET
return function(word)
local isEn = word:isEn()
local result = {}
local uri = ('https://v.api.aa1.cn/api/api-fanyi-yd/index.php?msg=%s&type=%d'):format(word, isEn and 2 or 1)
GET(uri, {
callback = function(str)
local ok, res = pcall(vim.json.decode, str)
if not ok or not res or not res.text or isEn and res.text:isEn() then
result[1] = false
return
end
result[1] = {
title = { word = word },
[isEn and 'translation' or 'definition'] = res.text,
}
if result.callback then
result.callback(result[1])
end
end
})
return result
end
-- local youdao = require("Trans").conf.engine.youdao
-- local uri = 'https://openapi.youdao.com/api'
-- local salt = tostring(math.random(bit.lshift(1, 15)))
-- local appid = youdao.appid
-- local appPasswd = youdao.appPasswd
-- local post = require('Trans.util.curl').POST
-- local function get_field(word)
-- -- local to = isEn and 'zh-'
-- local len = #word
-- local curtime = tostring(os.time())
-- local input = len > 20 and
-- word:sub(1, 10) .. len .. word:sub(-10) or word
-- -- sign=sha256(应用ID+input+salt+curtime+应用密钥)
-- local hash = appid .. input .. salt .. curtime .. appPasswd
-- local sign = vim.fn.sha256(hash)
-- return {
-- q = word,
-- from = 'auto',
-- to = 'zh-CHS',
-- signType = 'v3',
-- appKey = appid,
-- salt = salt,
-- curtime = curtime,
-- sign = sign,
-- }
-- end
-- return function(word)
-- -- return result
-- -- local field = get_field(word)
-- -- local output = post(uri, {
-- -- body = field,
-- -- })
-- -- if output.exit == 0 and output.status == 200 then
-- -- local result = vim.fn.json_decode(output.body)
-- -- if result and result.errorCode == 0 then
-- -- --- TODO :
-- -- end
-- -- end
-- end

View File

@ -44,6 +44,13 @@ return {
TransFailed = {
fg = '#7aa89f',
},
TransWaitting = {
link = 'MoreMsg'
},
TransWeb = {
-- TODO :
link = 'MoreMsg',
}
},
--- TODO :
@ -91,6 +98,9 @@ return {
TransFailed = {
fg = '#f4b085',
},
TransWaitting = {
link = 'MoreMsg'
},
},
dracula = {
TransWord = {
@ -136,5 +146,8 @@ return {
TransFailed = {
fg = '#8be9fd',
},
TransWaitting = {
link = 'MoreMsg'
},
},
}

View File

@ -1,89 +0,0 @@
--- TODO :wrapper for curl
local curl = {}
-- local example = {
-- data = {},
-- headers = {
-- k = 'v',
-- },
-- callback = function(output)
-- end,
-- }
curl.GET = function(uri, opts)
--- TODO :
vim.validate {
uri = { uri, 's' },
opts = { opts, 't' }
}
local cmd = {'curl', '-s', ('"%s"'):format(uri)}
local callback = opts.callback
local output = ''
local option = {
stdin = 'null',
on_stdout = function(_, stdout)
local str = table.concat(stdout)
if str ~= '' then
output = output .. str
end
end,
on_exit = function()
callback(output)
end,
}
vim.fn.jobstart(table.concat(cmd, ' '), option)
end
curl.POST = function(uri, opts)
vim.validate {
uri = { uri, 's' },
opts = { opts, 't' }
}
local callback = opts.callback
local cmd = { 'curl', '-s', ('"%s"'):format(uri) }
local size = 3
local function insert(...)
for _, v in ipairs { ... } do
size = size + 1
cmd[size] = v
end
end
local s = '"%s=%s"'
if opts.headers then
for k, v in pairs(opts.headers) do
insert('-H', s:format(k, v))
end
end
for k, v in pairs(opts.data) do
insert('-d', s:format(k, v))
end
local output = ''
local option = {
stdin = 'null',
on_stdout = function(_, stdout)
local str = table.concat(stdout)
if str ~= '' then
output = output .. str
end
end,
on_exit = function()
callback(output)
end,
}
vim.fn.jobstart(table.concat(cmd, ' '), option)
end
return curl

View File

@ -1,48 +0,0 @@
return function(opts)
local target = opts.times
opts.run = target ~= 0
---@type function[]
local tasks = {}
local function do_task()
for _, task in ipairs(tasks) do
task()
end
end
local frame
if target then
local times = 0
frame = function()
if opts.run and times < target then
times = times + 1
opts:frame(times)
vim.defer_fn(frame, opts.interval)
else
do_task()
end
end
else
frame = function()
if opts.run then
opts:frame()
vim.defer_fn(frame, opts.interval)
else
do_task()
end
end
end
frame()
---任务句柄, 如果任务结束了则立即执行, 否则立即执行
---@param task function
return function(task)
if opts.run then
tasks[#tasks + 1] = task
else
task()
end
end
end

View File

@ -1,3 +1,7 @@
---@class TransUtil
---@field md5 TransUtilMd5
---@class TransUtilMd5
local md5 = {}
-- local md5 = {
-- _VERSION = "md5.lua 1.1.0",

101
lua/Trans/util/node.lua Normal file
View File

@ -0,0 +1,101 @@
local util = require 'Trans'.util
---@class TransNode
---@field [1] string text to be rendered
---@field render fun(self: TransNode, buffer: TransBuffer, line: number, col: number) render the node
---@class TransItem : TransNode
local item_meta = {
---@param self TransItem
---@param buffer TransBuffer
---@param line integer
---@param col integer
render = function(self, buffer, line, col)
if self[2] then
buffer:add_highlight(line, self[2], col, col + #self[1])
end
end,
}
---@class TransText : TransNode
---@field step string
---@field nodes TransNode[]
local text_meta = {
---@param self TransText
---@param buffer TransBuffer
---@param line integer
---@param col integer
render = function(self, buffer, line, col)
local nodes = self.nodes
local step = self.step
local len = step and #step or 0
for _, node in ipairs(nodes) do
node:render(buffer, line, col)
col = col + #node[1] + len
end
end,
}
item_meta.__index = item_meta
text_meta.__index = text_meta
---Basic item node
---@param tuple {[1]: string, [2]: string?}
---@return TransItem
local function item(tuple)
return setmetatable(tuple, item_meta)
end
---@param nodes {[number]: TransNode, step: string?}
---@return TransText
local function text(nodes)
return setmetatable({
[1] = table.concat(util.list_fields(nodes, 1), nodes.step),
step = nodes.step,
nodes = nodes,
}, text_meta)
end
---@param args {[number]: TransNode, width: integer, spin: string?}
---@return TransText
local function format(args)
local width = args.width
local spin = args.spin or ' '
local size = #args
local wid = 0
for i = 1, size do
wid = wid + args[i][1]:width()
end
local space = math.max(math.floor((width - wid) / (size - 1)), 0)
args.step = spin:rep(space)
args.width = nil
args.spin = nil
---@diagnostic disable-next-line: param-type-mismatch
return text(args)
end
---@class TransUtil
---@field node TransNodes
---@class TransNodes
return {
item = item,
text = text,
format = format,
prompt = function(str)
return {
item { '', 'TransTitleRound' },
item { str, 'TransTitle' },
item { '', 'TransTitleRound' },
}
end,
}

View File

@ -1,120 +0,0 @@
local api = vim.api
local conf = require('Trans').conf
local buffer = require('Trans.buffer')()
local node = require("Trans.node")
local t = node.text
local it = node.item
local f = node.format
local engine_map = {
baidu = '百度',
youdao = '有道',
iciba = 'iciba',
offline = '本地',
}
local function set_tag_hl(name, status)
-- local hl = conf.float.tag[status]
-- m_window:set_hl(name, {
-- fg = '#000000',
-- bg = hl,
-- })
-- m_window:set_hl(name .. 'round', {
-- fg = hl,
-- })
end
local function set_title()
-- local title = m_window:new_content()
-- local github = ' https://github.com/JuanZoran/Trans.nvim'
-- title:addline(
-- title:center(it(github, '@text.uri'))
-- )
-- local f = '%s(%d)'
-- local tags = {}
-- local load_tag = function(engine, index)
-- set_tag_hl(engine, 'wait')
-- local round = engine .. 'round'
-- table.insert(tags, t(
-- it('', round),
-- it(f:format(engine_map[engine], index), engine),
-- it('', round)
-- ))
-- end
-- load_tag('offline', 1)
-- title:addline(unpack(tags))
-- title:newline('')
end
local action = {
quit = function()
-- m_window:try_close()
end,
}
local exist = function(str)
return str and str ~= ''
end
local function process()
-- TODO :
-- local icon = conf.icon
-- m_content:addline(m_content:format {
-- nodes = {
-- it(m_result.word, 'TransWord'),
-- t(
-- it('['),
-- it(exist(m_result.phonetic) and m_result.phonetic or icon.notfound, 'TransPhonetic'),
-- it(']')
-- ),
-- it(m_result.collins and icon.star:rep(m_result.collins) or icon.notfound, 'TransCollins'),
-- it(m_result.oxford == 1 and icon.yes or icon.no)
-- },
-- width = math.floor(m_window.width * 0.5)
-- })
-- m_content:addline(it('该窗口还属于实验性功能 .... '))
end
return function(word)
buffer:init()
-- TODO :online query
-- local float = conf.float
vim.notify([[
[注意]:
float窗口目前还待开发
如果需要input查询功能, 请将窗口改成hover]])
-- local opt = {
-- relative = 'editor',
-- width = float.width,
-- height = float.height,
-- border = float.border,
-- title = float.title,
-- animation = float.animation,
-- row = bit.rshift((vim.o.lines - float.height), 1),
-- col = bit.rshift((vim.o.columns - float.width), 1),
-- zindex = 20,
-- }
-- m_window = require('Trans.window')(true, opt)
-- set_title()
-- m_content = m_window:new_content()
-- m_result = require('Trans.query.offline')(word)
-- if m_result then
-- set_tag_hl('offline', 'success')
-- process()
-- else
-- set_tag_hl('offline', 'fail')
-- end
-- m_window:open()
-- m_window:bufset('bufhidden', 'wipe')
-- for act, key in pairs(float.keymap) do
-- m_window:map(key, action[act])
-- end
end

View File

@ -1,384 +0,0 @@
local api = vim.api
local conf = require('Trans').conf
local hover = conf.hover
local error_msg = conf.icon.notfound .. ' 没有找到相关的翻译'
local buffer = require('Trans.buffer')()
local node = require('Trans.node')
local it, t, f = node.item, node.text, node.format
local function handle_result(result)
local icon = conf.icon
local notfound = icon.notfound
local indent = ' '
local word = result.title.word
if hover.auto_play then
string.play(word:isEn() and word or result.definition)
end
local addtitle = function(title)
buffer:addline {
it('', 'TransTitleRound'),
it(title, 'TransTitle'),
it('', 'TransTitleRound'),
}
end
local process = {
title = function(title)
local oxford = title.oxford
local collins = title.collins
local phonetic = title.phonetic
if not phonetic and not collins and not oxford then
buffer:addline(it(word, 'TransWord'))
else
buffer:addline(f {
width = hover.width,
text = t {
it(word, 'TransWord'),
t {
it('['),
it((phonetic and phonetic ~= '') and phonetic or notfound, 'TransPhonetic'),
it(']')
},
it(collins and icon.star:rep(collins) or notfound, 'TransCollins'),
it(oxford == 1 and icon.yes or icon.no)
},
})
end
end,
tag = function(tag)
addtitle('标签')
local tag_map = {
zk = '中考',
gk = '高考',
ky = '考研',
cet4 = '四级',
cet6 = '六级',
ielts = '雅思',
toefl = '托福',
gre = 'gre ',
}
local tags = {}
local size = 0
local interval = ' '
for _tag in vim.gsplit(tag, ' ', true) do
size = size + 1
tags[size] = tag_map[_tag]
end
for i = 1, size, 3 do
buffer:addline(
it(
indent .. tags[i] ..
(tags[i + 1] and interval .. tags[i + 1] ..
(tags[i + 2] and interval .. tags[i + 2] or '') or ''),
'TransTag'
)
)
end
buffer:addline('')
end,
pos = function(pos)
addtitle('词性')
local pos_map = {
a = '代词pron ',
c = '连接词conj ',
i = '介词prep ',
j = '形容词adj ',
m = '数词num ',
n = '名词n ',
p = '代词pron ',
r = '副词adv ',
u = '感叹词int ',
v = '动词v ',
x = '否定标记not ',
t = '不定式标记infm ',
d = '限定词determiner ',
}
local s = '%s %2s%%'
for _pos in vim.gsplit(pos, '/', true) do
buffer:addline(
it(indent .. s:format(pos_map[_pos:sub(1, 1)], _pos:sub(3)), 'TransPos')
)
end
buffer:addline('')
end,
exchange = function(exchange)
addtitle('词形变化')
local exchange_map = {
['p'] = '过去式 ',
['d'] = '过去分词 ',
['i'] = '现在分词 ',
['r'] = '比较级 ',
['t'] = '最高级 ',
['s'] = '复数 ',
['0'] = '原型 ',
['1'] = '类别 ',
['3'] = '第三人称单数',
['f'] = '第三人称单数',
}
local interval = ' '
for exc in vim.gsplit(exchange, '/', true) do
buffer:addline(
it(indent .. exchange_map[exc:sub(1, 1)] .. interval .. exc:sub(3), 'TransExchange')
)
end
buffer:addline('')
end,
translation = function(translation)
addtitle('中文翻译')
for trs in vim.gsplit(translation, '\n', true) do
buffer:addline(
it(indent .. trs, 'TransTranslation')
)
end
buffer:addline('')
end,
definition = function(definition)
addtitle('英文注释')
for def in vim.gsplit(definition, '\n', true) do
def = def:gsub('^%s+', '', 1) -- TODO :判断是否需要分割空格
buffer:addline(
it(indent .. def, 'TransDefinition')
)
end
buffer:addline('')
end,
}
buffer:set('modifiable', true)
for _, field in ipairs(conf.order) do
local value = result[field]
if value and value ~= '' then
process[field](value)
end
end
buffer:set('modifiable', false)
end
local function open_window(opts)
opts = opts or {}
local col = opts.col or 1
local row = opts.row or 1
local width = opts.width or hover.width
local height = opts.height or hover.height
local relative = opts.relative or 'cursor'
return require('Trans.window') {
col = col,
row = row,
buf = buffer,
relative = relative,
width = width,
height = height,
title = hover.title,
border = hover.border,
animation = hover.animation,
ns = require('Trans').ns,
}
end
local function handle_keymap(win, word)
local keymap = hover.keymap
local cur_buf = api.nvim_get_current_buf()
local del = vim.keymap.del
local function try_del_keymap()
for _, key in pairs(keymap) do
pcall(del, 'n', key)
end
end
local lock = false
local cmd_id
local next
local action = {
pageup = function()
buffer:normal('gg')
end,
pagedown = function()
buffer:normal('G')
end,
pin = function()
if lock then
error('请先关闭窗口')
else
lock = true
end
pcall(api.nvim_del_autocmd, cmd_id)
local width, height = win.width, win.height
local col = vim.o.columns - width - 3
local buf = buffer.bufnr
local run = win:try_close()
run(function()
local w, r = open_window {
width = width,
height = height,
relative = 'editor',
col = col,
}
next = w.winid
win = w
r(function()
w:set('wrap', true)
end)
del('n', keymap.pin)
api.nvim_create_autocmd('BufWipeOut', {
callback = function(opt)
if opt.buf == buf or opt.buf == cur_buf then
lock = false
api.nvim_del_autocmd(opt.id)
end
end
})
end)
end,
close = function()
pcall(api.nvim_del_autocmd, cmd_id)
local run = win:try_close()
run(function()
buffer:delete()
end)
try_del_keymap()
end,
toggle_entry = function()
if lock and win:is_valid() then
local prev = api.nvim_get_current_win()
api.nvim_set_current_win(next)
next = prev
else
del('n', keymap.toggle_entry)
end
end,
play = function()
if word then
word:play()
end
end,
}
local set = vim.keymap.set
for act, key in pairs(hover.keymap) do
set('n', key, action[act])
end
if hover.auto_close_events then
cmd_id = api.nvim_create_autocmd(
hover.auto_close_events, {
buffer = 0,
callback = action.close,
})
end
end
local function online_query(win, word)
local lists = {
remove = table.remove
}
local engines = conf.engines
local size = #engines
local icon = conf.icon
local error_line = it(error_msg, 'TransFailed')
if size == 0 then
buffer:addline(error_line)
return
end
for i = 1, size do
lists[i] = require('Trans.query.' .. engines[i])(word)
end
local cell = icon.cell
local timeout = hover.timeout
local spinner = require('Trans.ui.spinner')[hover.spinner]
local range = #spinner
local interval = math.floor(timeout / (win.width - spinner[1]:width()))
local win_width = win.width
local s = '%s %s'
local width, height = hover.width, hover.height
local function waitting_result(this, times)
for i = size, 1, -1 do
local res = lists[i][1]
if res then
buffer:wipe()
win:set_width(width)
handle_result(res)
height = math.min(height, buffer:height(width))
win:expand {
field = 'height',
target = height,
}
this.run = false
return
elseif res == false then
lists:remove(i)
size = size - 1
end
end
if size == 0 or times == win_width then
buffer:addline(error_line, 1)
this.run = false
else
buffer:addline(it(s:format(spinner[times % range + 1], cell:rep(times)), 'MoreMsg'), 1)
end
end
buffer:set('modifiable', true)
local run = require('Trans.util.display') {
times = win_width,
interval = interval,
frame = waitting_result,
}
run(function()
buffer:set('modifiable', false)
end)
end
---处理不同hover模式的窗口
---@param word string 待查询的单词
return function(word)
buffer:init()
local result = require('Trans.query.offline')(word)
if result then
handle_result(result)
local width = hover.width
local win, run = open_window {
width = width,
height = math.min(buffer:height(width), hover.height)
}
run(function()
win:set('wrap', true)
handle_keymap(win, word)
end)
else
local win, run = open_window {
width = error_msg:width(),
height = 1,
}
run(function()
win:set('wrap', true)
handle_keymap(win, word)
online_query(win, word)
end)
end
end

View File

@ -1,224 +0,0 @@
local api = vim.api
local display = require('Trans.util.display')
---@class win
---@field winid integer window handle
---@field width integer
---@field height integer
---@field ns integer namespace for highlight
---@field animation table window animation
---@field buf buf buffer for attached
local window = {}
---Change window attached buffer
---@param buf buf
function window:set_buf(buf)
api.nvim_win_set_buf(self.winid, buf.bufnr)
self.buf = buf
end
---Check window valid
---@return boolean
function window:is_valid()
return api.nvim_win_is_valid(self.winid)
end
---Set window option
---@param option string option name
---@param value any
function window:set(option, value)
if self:is_valid() then
api.nvim_win_set_option(self.winid, option, value)
end
end
---@param name string option name
---@return any
function window:option(name)
return api.nvim_win_get_option(self.winid, name)
end
---@param height integer
function window:set_height(height)
api.nvim_win_set_height(self.winid, height)
self.height = height
end
---@param width integer
function window:set_width(width)
api.nvim_win_set_width(self.winid, width)
self.width = width
end
---Expand window [width | height] value
---@param opts table
---|'field'string [width | height]
---|'target'integer
---@return function
function window:expand(opts)
self:lock()
local field = opts.field
local target = opts.target
local cur = self[field]
local times = math.abs(target - cur)
local wrap = self:option('wrap')
self:set('wrap', false)
local interval = opts.interval or self.animation.interval
local method = api['nvim_win_set_' .. field]
local winid = self.winid
local frame = target > cur and function(_, cur_times)
method(winid, cur + cur_times)
end or function(_, cur_times)
method(winid, cur - cur_times)
end
local run = display {
times = times,
frame = frame,
interval = interval,
}
run(function()
self:set('wrap', wrap)
self[field] = target
self:unlock()
end)
return run
end
---Close window
---@return function run run until close done
function window:try_close()
local field = ({
slid = 'width',
fold = 'height',
})[self.animation.close]
--- 播放动画
local run = self:expand {
field = field,
target = 1,
}
run(function()
api.nvim_win_close(self.winid, true)
end)
return run
end
---lock window [open | close] operation
function window:lock()
while self.busy do
vim.wait(50)
end
self.busy = true
end
function window:unlock()
self.busy = false
end
---设置窗口本地的高亮组
---@param name string 高亮组的名称
---@param opts table 高亮选项
function window:set_hl(name, opts)
api.nvim_set_hl(self.ns, name, opts)
end
---buffer:addline() helper function
---@param node table
---@return table node formatted node
function window:center(node)
local text = node[1]
local width = text:width()
local win_width = self.width
local space = math.max(math.floor((win_width - width) / 2), 0)
node[1] = (' '):rep(space) .. text
return node
end
---@private
window.__index = window
---@class win_opts
---@field buf buf buffer for attached
---@field height integer
---@field width integer
---@field col integer
---@field row integer
---@field border string
---@field title string | nil | table
---@field relative string
---@field ns integer namespace for highlight
---@field zindex? integer
---@field enter? boolean cursor should [enter] window
---@field animation table window animation
---window constructor
---@param opts win_opts
---@return table
---@return function
return function(opts)
assert(type(opts) == 'table')
local ns = opts.ns
local buf = opts.buf
local col = opts.col
local row = opts.row
local title = opts.title
local width = opts.width
local enter = opts.enter or false
local height = opts.height
local border = opts.border
local zindex = opts.zindex
local relative = opts.relative
local animation = opts.animation
local open = animation.open
local field = ({
slid = 'width',
fold = 'height',
})[open]
local win_opt = {
title_pos = nil,
focusable = false,
style = 'minimal',
zindex = zindex,
width = width,
height = height,
col = col,
row = row,
border = border,
title = title,
relative = relative,
}
if field then
win_opt[field] = 1
end
if win_opt.title then
win_opt.title_pos = 'center'
end
local win = setmetatable({
buf = buf,
ns = ns,
height = win_opt.height,
width = win_opt.width,
animation = animation,
winid = api.nvim_open_win(buf.bufnr, enter, win_opt),
}, window)
api.nvim_win_set_hl_ns(win.winid, win.ns)
win:set_hl('Normal', { link = 'TransWin' })
win:set_hl('FloatBorder', { link = 'TransBorder' })
return win, win:expand {
field = field,
target = opts[field],
}
end

144
lua/test/buffer_spec.lua Normal file
View File

@ -0,0 +1,144 @@
require 'test.setup'
describe('buffer:setline()', function()
it('can accept one index linenr as second arg', with_buffer(function(buffer)
buffer:setline({
i { 'hello ' },
i { 'world' },
}, 1)
assert.are.equal(buffer[1], 'hello world')
end))
it('will append line when no second arg passed', with_buffer(function(buffer)
buffer[1] = 'hello'
buffer:setline 'world'
assert.are.equal(buffer[2], 'world')
end))
describe('and buffer[i]', function()
it('can accept a string as first arg', with_buffer(function(buffer)
buffer:setline 'hello world'
buffer[2] = 'hello world'
assert.are.equal(buffer[1], 'hello world')
assert.are.equal(buffer[2], 'hello world')
end))
it('can accept a node as first arg', with_buffer(function(buffer)
buffer:setline(i { 'hello world' })
buffer[2] = i { 'hello world' }
assert.are.equal(buffer[1], 'hello world')
assert.are.equal(buffer[2], 'hello world')
end))
it('can accept a node list as first arg', with_buffer(function(buffer)
buffer:setline {
i { 'hello ' },
i { 'world' },
}
buffer[2] = {
i { 'hello ' },
i { 'world' },
}
assert.are.equal(buffer[1], 'hello world')
assert.are.equal(buffer[2], 'hello world')
end))
it(' will fill with empty line if linenr is more than line_count', with_buffer(function(buffer)
buffer:setline('hello world', 3)
buffer[4] = 'hello world'
assert.are.equal(buffer[1], '')
assert.are.equal(buffer[2], '')
assert.are.equal(buffer[3], 'hello world')
assert.are.equal(buffer[4], 'hello world')
buffer:wipe()
buffer[1] = i { 'test' }
buffer[3] = 'hello world'
assert.are.equal(buffer[1], 'test')
assert.are.equal(buffer[2], '')
assert.are.equal(buffer[3], 'hello world')
end))
end)
end)
-- TODO :Add node test
describe('buffer:deleteline()', with_buffer(function(buffer)
before_each(function()
buffer:wipe()
end)
it('will delete the last line if no arg', function()
buffer[1] = 'line 1'
buffer[2] = 'line 2'
buffer:deleteline()
assert.are.equal(buffer:line_count(), 1)
assert.are.equal(buffer[1], 'line 1')
buffer:deleteline()
assert.are.equal(buffer:line_count(), 0)
end)
it('can accept a one indexed linenr to be deleted', function()
buffer[1] = 'line 1'
buffer[2] = 'line 2'
buffer:deleteline(1)
assert.are.equal(buffer[1], 'line 2')
end)
it('can accept a one indexed range to be deleted', function()
stub(api, 'nvim_buf_set_lines')
buffer[1] = 'line 1'
buffer[2] = 'line 2'
buffer[3] = 'line 3'
buffer:deleteline(1, 2)
---@diagnostic disable-next-line: param-type-mismatch
assert.stub(api.nvim_buf_set_lines).called_with(buffer.bufnr, 0, 2, false, {})
api.nvim_buf_set_lines:revert()
buffer:deleteline(1, 2)
assert.are.equal(buffer[1], 'line 3')
end)
end))
describe('buffer:lines()', with_buffer(function(buffer)
before_each(function()
buffer:wipe()
end)
it('will return all lines if no arg', function()
buffer[1] = 'line 1'
buffer[2] = 'line 2'
local lines = buffer:lines()
assert.are.equal(lines[1], 'line 1')
assert.are.equal(lines[2], 'line 2')
end)
it('will return all lines after linenr accept a one indexed linenr', function()
buffer[1] = 'line 1'
buffer[2] = 'line 2'
buffer[3] = 'line 3'
buffer[4] = 'line 4'
local lines = buffer:lines(2)
assert.are.equal(lines[1], 'line 2')
assert.are.equal(lines[2], 'line 3')
assert.are.equal(lines[3], 'line 4')
end)
it('can accept a one indexed range', function()
buffer[1] = 'line 1'
buffer[2] = 'line 2'
buffer[3] = 'line 3'
local lines = buffer:lines(1, 2)
assert.are.equal(lines[1], 'line 1')
assert.are.equal(lines[2], 'line 2')
lines = buffer:lines(2, 3)
assert.are.equal(lines[1], 'line 2')
assert.are.equal(lines[2], 'line 3')
end)
end))

18
lua/test/setup.lua Normal file
View File

@ -0,0 +1,18 @@
_G.Trans = require 'Trans'
local node = Trans.util.node
_G.i, _G.t, _G.pr, _G.f = node.item, node.text, node.prompt, node.format
_G.api = vim.api
_G.fn = vim.fn
_G.mock = require 'luassert.mock'
_G.stub = require 'luassert.stub'
---@param func fun(buffer: TransBuffer)
---@return fun()
function _G.with_buffer(func)
return function()
local buffer = Trans.buffer.new()
func(buffer)
buffer:destroy()
end
end

9
lua/test/util_spec.lua Normal file
View File

@ -0,0 +1,9 @@
require 'test.setup'
local util = Trans.util
describe('util.display_height', with_buffer(function(buffer)
--- TODO :
it('can calculate the height of lines when window with wrap option', function()
end)
end))

4
makefile Normal file
View File

@ -0,0 +1,4 @@
.PHONE: test
test:
nvim --headless --noplugin -u script/minimal_init.vim -c "PlenaryBustedDirectory lua/test/ { minimal_init = './scripts/minimal_init.vim' }" -c 'qa!'

View File

@ -1,12 +1,34 @@
local M = require("Trans")
local api = vim.api
local api, fn = vim.api, vim.fn
--- INFO :Define plugin command
local Trans = require("Trans")
local command = api.nvim_create_user_command
command('Translate', function() M.translate() end, { desc = ' 单词翻译', })
command('TranslateInput', function() M.translate('i') end, { desc = ' 搜索翻译', })
command('TransPlay', function()
local word = M.get_word(api.nvim_get_mode().mode)
if word ~= '' and word:isEn() then
word:play()
command("Translate", function()
Trans.translate()
end, { desc = "Translate cursor word" })
command("TranslateInput", function()
Trans.translate({ mode = 'i' })
end, { desc = " Translate input word" })
command("TransPlay", function()
local util = Trans.util
local str = util.get_str(vim.fn.mode())
if str and str ~= "" and util.is_English(str) then
str:play()
end
end, { desc = ' 自动发音' })
end, { desc = " Auto play" })
string.width = api.nvim_strwidth
local f =
fn.has('linux') == 1 and ([[echo %q | festival --tts]])
or fn.has('mac') == 1 and ([[say %q]])
or 'node' .. Trans.relative_path { 'tts', 'say.js' } .. ' %q'
string.play = function(self)
fn.jobstart(f:format(self))
end

5
script/minimal_init.vim Normal file
View File

@ -0,0 +1,5 @@
set rtp+=.
set rtp+=../plenary.nvim/
runtime! plugin/plenary.vim
runtime! plugin/Trans.lua