Compare commits

..

13 Commits
v2 ... main

Author SHA1 Message Date
JuanZoran
6a9d887db7 refactor: add warning message 2023-04-07 19:56:07 +08:00
JuanZoran
4d547a0397 docs: 注意: 此分支已经不再打算维护, 新版本目前在expermental分支, 稳定后会设置成默认分支, expermantal分支已经加入了百度和有道在线翻译 2023-03-17 10:31:12 +08:00
JuanZoran
b2851cffd8 feat: add health check and update README.md 2023-03-07 22:31:32 +08:00
JuanZoran
c6ad825dac fix: fix close window twice 2023-02-18 13:24:13 +08:00
JuanZoran
c6c5bf4f7c fix: fix window is not valid 2023-02-18 13:13:32 +08:00
DeepSource Bot
ce43dbd489 Add .deepsource.toml 2023-02-16 17:18:16 +00:00
JuanZoran
adfbe7f50c docs: 添加mac发音感谢说明 2023-02-08 13:23:25 +08:00
Zoran
5355d9c97e
Merge pull request #24 from happysmile12321/main
add mac specify commit
2023-02-08 13:19:52 +08:00
happysmile
2172d29f08 add mac specify commit 2023-02-08 12:17:36 +08:00
JuanZoran
c35cfbb0f5 chore: merge branch 'expermental' 2023-02-07 17:18:52 +08:00
JuanZoran
e723f4177f Merge branch 'experimental' 2023-02-07 17:15:54 +08:00
JuanZoran
b62478cf2d fix: 取消了回调的设计, 修复了部分bug
将回调的接口换成了run的函数接口, 修复了在线查询, 自动命令, 窗口显示的bug
2023-02-03 15:33:32 +08:00
JuanZoran
acfde8e4f5 Merge branch 'experimental' 2023-02-01 17:18:51 +08:00
58 changed files with 2150 additions and 4522 deletions

7
.deepsource.toml Normal file
View File

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

View File

@ -1,51 +0,0 @@
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,6 +4,3 @@ demo.mp4
screenshot.gif
tts/node_modules/
tts/package-lock.json
Trans.json
ultimate.db
lua/.luarc.json

21
LICENCE
View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2023 Zoran
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

399
README.md
View File

@ -6,8 +6,6 @@
- [特点](#特点)
- [屏幕截图](#屏幕截图)
- [演示](#演示)
- [离线查询](#离线查询)
- [在线查询演示 (有道)](#在线查询演示-有道)
- [主题](#主题)
- [安装](#安装)
- [配置](#配置)
@ -16,55 +14,59 @@
- [声明](#声明)
- [感谢](#感谢)
- [贡献](#贡献)
- [从 v1 (main)分支迁移](#从-v1-main分支迁移)
- [待办 (画大饼)](#待办-画大饼)
- [项目情况](#项目情况)
<!--toc:end-->
> **插件默认词库的路径为插件目录**
例如: `lazy` 用户应该在 `$HOME/.local/share/nvim/lazy/Trans.nvim`
## 注意: 此分支已经不再打算维护, 新版本目前在expermental分支, 稳定后会设置成默认分支
## 特点
- `使用纯 lua 编写`
- 使用纯 lua 编写, 速度极快
> `Lazy.nvim`的记录: <font color="#0099FF">`➜  Trans.nvim 0.82ms`</font>
- **可以定义快捷键读英文单词**
> 见 wiki
- 大部分功能可以自定义:
- 🔍 高亮
- 👀 悬浮大小
- 📜 排版顺序
- 💬 弹窗大小
- 🎉 舒服窗口动画
- 更多可以查看[配置](#配置)
- `离线``在线`翻译的支持
- 高亮
- 悬浮大小
- 排版顺序
- 弹窗大小
- `舒服窗口动画`
- etc (更多可以查看[配置](#配置))
- **完全离线** 的单词翻译体验 (可能后面会支持在线翻译)
- 支持显示:
- 🌟 柯林斯星级
- 📚 牛津 3000 词汇
- 🇨🇳 中文翻译
- 🇺🇸 英文翻译 (不是英译中, 而是用英文解释)
- 🌱 词根
- 柯林斯星级
- 牛津 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/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)
https://user-images.githubusercontent.com/107862700/213752097-2eee026a-ddee-4531-bf80-ba2cbc8b44ef.mp4
### 主题
@ -85,7 +87,7 @@ https://user-images.githubusercontent.com/107862700/226176106-c2962dd3-d66c-499c
_安装之前, 首先需要明确本插件的依赖:_
- [ECDICT](https://github.com/skywind3000/ECDICT): 插件所用的离线单词数据库
- [sqlite.lua](https://github.com/kkharji/sqlite.lua): 操作数据库所用的库
- sqlite.lua: 操作数据库所用的库
- sqlite3: 数据库
<details>
@ -93,9 +95,9 @@ _安装之前, 首先需要明确本插件的依赖:_
```lua
use {
'JuanZoran/Trans.nvim',
run = function() require('Trans').install() end, -- 自动下载使用的本地词库
requires = 'kkharji/sqlite.lua', ,
'JuanZoran/Trans.nvim'
run = 'bash ./install.sh',
requires = 'kkharji/sqlite.lua',
-- 如果你不需要任何配置的话, 可以直接按照下面的方式启动
config = function ()
require'Trans'.setup{
@ -115,15 +117,13 @@ use {
{ {'n', 'x'}, 'mk' },
{ 'n', 'mi' },
},
run = function() require('Trans').install() end, -- 自动下载使用的本地词库
requires = { 'kkharji/sqlite.lua', },
run = 'bash ./install.sh', -- 自动下载使用的本地词库
requires = 'kkharji/sqlite.lua',
config = function()
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>')
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' })
end
}
```
@ -136,15 +136,15 @@ use {
```lua
{
"JuanZoran/Trans.nvim",
build = function () require'Trans'.install() end,
keys = {
-- 可以换成其他你想映射的键
{ 'mm', mode = { 'n', 'x' }, '<Cmd>Translate<CR>', desc = '󰊿 Translate' },
{ 'mk', mode = { 'n', 'x' }, '<Cmd>TransPlay<CR>', desc = ' Auto Play' },
{ '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' },
{ 'mi', '<Cmd>TranslateInput<CR>', desc = ' Translate From Input' },
},
dependencies = { 'kkharji/sqlite.lua', },
dependencies = { 'kkharji/sqlite.lua', lazy = true },
opts = {
-- your configuration there
}
@ -155,31 +155,47 @@ use {
<font color="#FF9900">**注意事项**: </font>
- 下载词典的过程中, 需要能够 `流畅的访问github下载`
- **如果插件无法正常工作, 请运行**`:check Trans`, 查看插件是否安装正确并且处于正常工作环境
如果下载出现问题, 正常是会自动下载
- `install.sh`
- 使用了 `wget`下载词库, 安装请确保你的环境变量中存在 wget
- install.sh 下载后会自动将词库解压, 并移动到 `$HOME/.vim/dict`文件夹下
- 目前仅在 `Ubuntu22.04`的环境下测试通过
> 如果上述条件不符合, 请删掉 `run = 'install.sh'`部分, 考虑手动安装词库
> 如果上述条件满足, 仍出现问题, 欢迎在 issue 里向我反馈,我会及时尝试解决
- 下载词典的过程中, 需要能够 `流畅的访问github下载`
> 词库文件压缩包大小为: **281M**
> 解压缩后的大小大概为: **1.2G**
- 安装后如果不能正常运行, 清尝试运行 `checkhealth Trans`
- 安装后如果不能正常运行, 请尝试检查一下问题:
- **`auto_play`** 的使用:
- 本机是否已经安装了 `sqlite3`
- `Linux` 需要安装`festival`
> Linux 下安装:
> `sudo pacman -S sqlite # Arch`
> `sudo apt-get install sqlite3 libsqlite3-dev # Ubuntu`
> `sudo apt-get install festival festvox-kallpc16k`
- **`auto_play`** 使用步骤:
**如果你想要设置音色,发音可以访问:** [Festival 官方](https://www.cstr.ed.ac.uk/projects/festival/morevoices.html)
可以选择英音、美音、男声、女声
> linux 只需要安装`festival`
> sudo apt-get install festival festvox-kallpc16k
> **_如果你想要设置音色发音可以访问:_** [Festival 官方](https://www.cstr.ed.ac.uk/projects/festival/morevoices.html)
> 可以选择英音、美音、男声、女声
- `Termux` 需要安装`termux-api`
> mac 系统使用`say` (感谢[@happysmile12321](https://github.com/happysmile12321) )
- `Mac` 使用系统的`say`命令
- `Windows` 使用原生的 Powershell 命令, 感谢[PR](https://github.com/JuanZoran/Trans.nvim/pull/34)
> 其他操作系统
- 需要确保安装了`nodejs`
- 进入插件的`tts`目录运行`npm install`
> 如果`install.sh`运行正常则自动安装,如果安装失败,请尝试手动安装
- `title`的配置,只对`neovim 0.9+`版本有效
- `title`的配置,只对`neovim 0.9`版本有效
<details>
<summary>Festival配置(仅针对linux用户)</summary>
@ -231,83 +247,66 @@ use {
## 配置
详细见**wiki**: [基本配置说明](https://github.com/JuanZoran/Trans.nvim/wiki/%E9%85%8D%E7%BD%AE)
<details>
<summary>默认配置</summary>
```lua
default_conf = {
---@type string the directory for database file and password file
dir = require 'Trans'.plugin_dir,
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 = '*',
require'Trans'.setup {
view = {
i = 'float',
n = 'hover',
v = 'hover',
},
},
---@type table frontend options
frontend = {
---@class TransFrontendOpts
---@field keymaps table<string, string>
default = {
query = 'fallback',
hover = {
width = 37,
height = 27,
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
title = title,
keymap = {
pageup = '[[',
pagedown = ']]',
pin = '<leader>[',
close = '<leader>]',
toggle_entry = '<leader>;',
play = '_',
},
animation = {
open = 'slid', -- 'fold', 'slid'
-- open = 'fold',
-- close = 'fold',
open = '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 = {
-- INFO : No default keymaps anymore, please set it yourself
-- pageup = '<C-u>',
-- pagedown = '<C-d>',
-- 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',
auto_play = true,
timeout = 3000,
spinner = 'dots', -- 查看所有样式: /lua/Trans/util/spinner
-- spinner = 'moon'
},
offline = {
float = {
width = 0.8,
height = 0.8,
border = 'rounded',
title = title,
keymap = {
quit = 'q',
},
animation = {
open = 'fold',
close = 'fold',
interval = 10,
},
tag = {
wait = '#519aba',
fail = '#e46876',
success = '#10b981',
},
engine = {
'本地',
}
},
order = { -- only work on hover mode
'title',
'tag',
'pos',
@ -315,34 +314,46 @@ default_conf = {
'translation',
'definition',
},
youdao = {
'title',
'translation',
'definition',
'web',
},
},
icon = {
-- or use emoji
list = '●', -- ● | ○ | ◉ | ◯ | ◇ | ◆ | ▪ | ▫ | ⬤ | 🟢 | 🟡 | 🟣 | 🟤 | 🟠| 🟦 | 🟨 | 🟧 | 🟥 | 🟪 | 🟫 | 🟩 | 🟦
star = '', -- ⭐ | ✴ | ✳ | ✲ | ✱ | ✰ | ★ | ☆ | 🌟 | 🌠 | 🌙 | 🌛 | 🌜 | 🌟 | 🌠 | 🌌 | 🌙 |
notfound = '󰆆 ', --❔ | ❓ | ❗ | ❕|
yes = '✔', -- ✅ | ✔️ | ☑
no = '', -- ❌ | ❎ | ✖ | ✘ | ✗ |
cell = '■', -- ■ | □ | ▇ | ▏ ▎ ▍ ▌ ▋ ▊ ▉
web = '󰖟', --🌍 | 🌎 | 🌏 | 🌐 |
tag = '',
pos = '',
exchange = '',
definition = '󰗊',
translation = '󰊿',
star = '',
notfound = ' ',
yes = '✔',
no = '',
-- --- char: ■ | □ | ▇ | ▏ ▎ ▍ ▌ ▋ ▊ ▉ █
-- --- ◖■■■■■■■◗▫◻ ▆ ▆ ▇⃞ ▉⃞
cell = '■',
-- star = '⭐',
-- notfound = '❔',
-- yes = '✔️',
-- no = '❌'
},
},
},
}
```
theme = 'default',
-- theme = 'dracula',
-- theme = 'tokyonight',
</details>
db_path = '$HOME/.vim/dict/ultimate.db',
engine = {
-- baidu = {
-- appid = '',
-- appPasswd = '',
-- },
-- -- youdao = {
-- appkey = '',
-- appPasswd = '',
-- },
},
-- TODO :
-- register word
-- history = {
-- -- TOOD
-- }
-- TODO :add online translate engine
}
```
## 快捷键绑定
@ -351,95 +362,64 @@ default_conf = {
> 示例中展示, 将`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>')
```
**窗口快捷键**
```lua
require 'Trans'.setup {
frontend = {
hover = {
keymaps = {
-- pageup = 'whatever you want',
-- pagedown = 'whatever you want',
-- pin = 'whatever you want',
-- close = 'whatever you want',
-- toggle_entry = 'whatever you want',
},
},
},
}
}
```
> 当窗口没有打开的时候, key 会被使用`vim.api.nvim_feedkey`来执行
## 高亮组
所有主题可见 `lua/Trans/style/theme.lua`
<details>
<summary>默认主题</summary>
> 默认定义
```lua
{
TransWord = {
fg = '#7ee787',
bold = true,
}
},
TransPhonetic = {
link = 'Linenr'
}
},
TransTitle = {
fg = '#0f0f15',
bg = '#75beff',
bold = true,
}
},
TransTitleRound = {
fg = '#75beff',
}
},
TransTag = {
-- fg = '#e5c07b',
link = '@tag'
}
fg = '#e5c07b',
},
TransExchange = {
link = 'TransTag',
}
},
TransPos = {
link = 'TransTag',
}
},
TransTranslation = {
link = 'TransWord',
}
},
TransDefinition = {
link = 'Moremsg',
}
},
TransWin = {
link = 'Normal',
}
},
TransBorder = {
fg = '#89B4FA',
}
link = 'FloatBorder',
},
TransCollins = {
fg = '#faf743',
bold = true,
}
},
TransFailed = {
fg = '#7aa89f',
}
TransWaitting = {
link = 'MoreMsg'
}
TransWeb = {
link = 'MoreMsg',
},
}
```
</details>
## 声明
- 本插件词典基于[ECDICT](https://github.com/skywind3000/ECDICT)
@ -452,25 +432,16 @@ TransWeb = {
## 贡献
> 更新比较频繁, 文档先鸽了 (wiki 写了一小部分
> 更新比较频繁, 文档先鸽了
> 如果你想要参加这个项目, 可以提 issue, 我会把文档补齐
## 从 v1 (main)分支迁移
见[wiki](<https://github.com/JuanZoran/Trans.nvim/wiki/%E4%BB%8E(v1)main%E5%88%86%E6%94%AF%E8%BF%81%E7%A7%BB>)
## 待办 (画大饼)
- [x] 多风格样式查询
- [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)
- [ ] 在线多引擎异步查询
- [ ] `句子翻译` | `中翻英` 的支持

View File

@ -1,435 +0,0 @@
*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:

18
install.sh Executable file
View File

@ -0,0 +1,18 @@
#!/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

13
lua/.luarc.json Normal file
View File

@ -0,0 +1,13 @@
{
"$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
}

View File

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

View File

@ -1,23 +0,0 @@
# 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
已知问题:
1. 缓存了的单词, 无法使用toggle_entry 进入页面
2. 加载配置需要输入所有表的key
- default_strategy can't deal with table correctly

View File

@ -1,73 +0,0 @@
---@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

@ -1,46 +0,0 @@
---@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

@ -1,183 +0,0 @@
local Trans = require 'Trans'
local db = require 'sqlite.db'
local path = Trans.conf.dir .. '/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'] = '最高级 ',
['b'] = '比较级 ',
['z'] = '最高级 ',
['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

@ -1,232 +0,0 @@
---@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"
-- }
-- }

165
lua/Trans/buffer.lua Normal file
View File

@ -0,0 +1,165 @@
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

@ -1,61 +0,0 @@
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,
})

View File

@ -1,212 +0,0 @@
local api, fn = vim.api, vim.fn
---@class TransBuffer
---@field bufnr integer buffer handle
---@field [integer] 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()
pcall(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)
return line_count == 1 and self[1] == '' and 0 or 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, values)
if type(key) == 'number' then
self:setline(values, key)
else
rawset(self, key, values)
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

View File

@ -1,139 +0,0 @@
---@class Trans
---@field conf TransConf
---@class TransConf
return {
---@type string the directory for database file and password file
dir = require 'Trans'.plugin_dir,
db_url = 'https://github.com/skywind3000/ECDICT-ultimate/releases/download/1.0.0/ecdict-ultimate-sqlite.zip',
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 = '<C-u>',
-- pagedown = '<C-d>',
-- 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 = {
-- "████████╗██████╗ █████╗ ███╗ ██╗███████╗",
-- "╚══██╔══╝██╔══██╗██╔══██╗████╗ ██║██╔════╝",
-- " ██║ ██████╔╝███████║██╔██╗ ██║███████╗",
-- " ██║ ██╔══██╗██╔══██║██║╚██╗██║╚════██║",
-- " ██║ ██║ ██║██║ ██║██║ ╚████║███████║",
-- " ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝",
--}

View File

@ -1,90 +0,0 @@
---@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

View File

@ -1,83 +0,0 @@
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

@ -1,32 +0,0 @@
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

@ -1,75 +0,0 @@
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

@ -1,50 +0,0 @@
---@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 = Trans.conf.db_url
local zip = dir .. '/ultimate.zip'
local continue = fn.filereadable(zip) == 1
local handle = function(output)
if output.exit == 0 and fn.filereadable(zip) then
local cmd =
Trans.system == 'win' and
string.format('powershell.exe -Command "Expand-Archive -Force %s %s"', zip, dir) or
fn.executable('unzip') == 1 and string.format('unzip %s -d %s', zip, dir) or
error('unzip not found, Please unzip ' .. zip .. ' manually')
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)
end

View File

@ -1,55 +0,0 @@
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)
end

View File

@ -1,104 +0,0 @@
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 and update(backend) do
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)
if not opts.str or opts.str == '' then return end
local str = opts.str:match("(%w+)")
-- 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

View File

@ -1,252 +0,0 @@
local fn, api = vim.fn, vim.api
---@class TransUtil
local M = require 'Trans'.metatable 'util'
---Get the range of visual modes
---@return table
function M.get_range()
local _start = fn.getpos 'v'
local _end = fn.getpos '.'
local s_row, e_row = math.min(_start[2], _end[2]), math.max(_start[2], _end[2])
local s_col, e_col = math.min(_start[3], _end[3]), math.max(_start[3], _end[3])
return { s_row, e_row, s_col, e_col }
end
---Get selected text
---@return string
function M.get_select()
local s_row, e_row, s_col, e_col = unpack(M.get_range())
---@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 selected text
---@return string
function M.get_lines()
local s_row, e_row = unpack(M.get_range())
if s_row == e_row then
return fn.getline(s_row)
else
local lines = fn.getline(s_row, e_row)
return table.concat(lines, " ")
end
end
---Get selected text
---@return string
function M.get_block()
local s_row, e_row, s_col, e_col = unpack(M.get_range())
---@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)
for col, l in pairs(lines) do
lines[col] = l:sub(s_col,e_col)
end
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()
api.nvim_input '<Esc>'
return M.get_lines()
end,
[''] = function()
api.nvim_input '<Esc>'
return M.get_block()
end,
})[mode]():match '^%s*(.-)%s*$'
end
---Puase coroutine for {ms} milliseconds
---@param ms integer
function M.pause(ms)
assert(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]
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:find '%W' == nil
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

View File

@ -1,228 +0,0 @@
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
pcall(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: 'mouse'|'cursor'|'editor'|'win', width: integer, height: integer, col: integer, row: integer, zindex?: integer, title?: table | string}
---@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,
-- }
local default_opts = {
enter = false,
winid = -1,
---@type WindowOpts
win_opts = {
-- INFO : ensured options
-- col
-- row
-- width
-- height
-- relative
-- zindex
style = 'minimal',
border = 'rounded',
focusable = true,
noautocmd = true,
},
}
---@class TransWindowOpts
---@field buffer TransBuffer attached buffer object
---@field enter? boolean cursor should [enter] window when open,default: false
---@field win_opts WindowOpts window config [**When open**]
---@field animation? table? Hover Window Animation
---Create new window
---@param opts TransWindowOpts window config
---@return TransWindow
function window.new(opts)
opts = vim.tbl_deep_extend('keep', opts, default_opts)
opts.animation = opts.animation or { interval = 12 }
local win = setmetatable(opts, window)
---@cast win TransWindow
win:open()
return win
end
---@class Trans
---@field window TransWindow
return window

View File

@ -1,123 +0,0 @@
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

@ -1,53 +0,0 @@
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

@ -1,258 +0,0 @@
---@type Trans
local Trans = require 'Trans'
local util = Trans.util
-- 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
---Set up function which will be invoked when this module is loaded
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
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes(key, true, true, true), 'n', false)
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 = {
relative = opts.relative or 'cursor',
col = opts.col or 1,
row = opts.row or 1,
width = opts.width or m_opts.width,
height = opts.height or m_opts.height,
title = m_opts.title,
title_pos = m_opts.title and 'center',
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 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 / times)
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] = 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()
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 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

@ -1,72 +0,0 @@
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

@ -1,96 +0,0 @@
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

@ -1,67 +0,0 @@
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,100 +1,63 @@
local Trans = require 'Trans'
local health, fn = vim.health, vim.fn
local M = {}
M.check = function()
local health = vim.health
local ok = health.report_ok
local warn = health.report_warn
local error = health.report_error
local has = fn.has
local executable = fn.executable
local function check_neovim_version()
if has 'nvim-0.9' == 1 then
ok [[You have [neovim-nightly] ]]
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
]]
else
warn [[Trans Title requires Neovim 0.9 or newer
See neovim-nightly: [https://github.com/neovim/neovim/releases/tag/nightly]
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
]]
end
end
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',
'unzip',
}
binary_dependencies[3] = ({
win = nil,
mac = 'say',
linux = 'festival',
termux = 'termux-tts-speak',
})[Trans.system]
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 .. '/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.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 }
return M

View File

@ -1,42 +1,252 @@
---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,
})
local M = {}
local api, fn = vim.api, vim.fn
if fn.executable 'sqlite3' ~= 1 then
error 'Please check out sqlite3'
end
---@class string
---@field width function @Get string display width
---@field play function @Use tts to play string
local win_title = fn.has 'nvim-0.9' == 1 and {
{ '', 'TransTitleRound' },
{ ' Trans', 'TransTitle' },
{ '', 'TransTitleRound' },
} or nil
local uname = vim.loop.os_uname().sysname
local system =
uname == 'Darwin' and 'mac' or
uname == 'Windows_NT' and 'win' or
uname == 'Linux' and (vim.fn.executable 'termux-api-start' == 1 and 'termux' or 'linux') or
error 'Unknown System, Please Report Issue'
-- local title = {
-- "████████╗██████╗ █████╗ ███╗ ██╗███████╗",
-- "╚══██╔══╝██╔══██╗██╔══██╗████╗ ██║██╔════╝",
-- " ██║ ██████╔╝███████║██╔██╗ ██║███████╗",
-- " ██║ ██╔══██╗██╔══██║██║╚██╗██║╚════██║",
-- " ██║ ██║ ██║██║ ██║██║ ╚████║███████║",
-- " ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝",
--}
---@class Trans
---@field style table @Style module
---@field cache table<string, TransData> @Cache for translated data object
---@field plugin_dir string @Plugin directory
---@field system 'mac'|'win'|'termux'|'linux' @Operating system
local M = metatable('core', {
cache = {},
style = metatable 'style',
system = system,
plugin_dir = vim.fn.fnamemodify(debug.getinfo(1, 'S').source:sub(2), ':p:h:h:h'),
})
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
end
M.metatable = metatable
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

73
lua/Trans/node.lua Normal file
View File

@ -0,0 +1,73 @@
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,
}

65
lua/Trans/query/baidu.lua Normal file
View File

@ -0,0 +1,65 @@
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

@ -0,0 +1,47 @@
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

@ -0,0 +1,74 @@
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

@ -1,497 +0,0 @@
-- Spinners adapted from: https://github.com/sindresorhus/cli-spinners
--
-- Some designs' names are made more descriptive; differences noted in comments.
-- Other designs are omitted for brevity.
--
-- You may want to adjust spinner_rate according to the number of frames of your
-- chosen spinner.
-- MIT License
--
-- Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
--
-- Permission is hereby granted, free of charge, to any person obtaining a copy
-- of this software and associated documentation files (the "Software"), to deal
-- in the Software without restriction, including without limitation the rights
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-- copies of the Software, and to permit persons to whom the Software is
-- furnished to do so, subject to the following conditions:
--
-- The above copyright notice and this permission notice shall be included in
-- all copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-- SOFTWARE.
return {
dots = {
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
},
dots_negative = { -- dots2
'',
'',
'',
'',
'',
'',
'',
'',
},
dots_snake = { -- dots3
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
},
dots_footsteps = { -- dots10
'',
'',
'',
'',
'',
'',
'',
},
dots_hop = { -- dots11
'',
'',
'',
'',
'',
'',
'',
'',
},
line = {
'-',
'\\',
'|',
'/',
},
pipe = {
'',
'',
'',
'',
'',
'',
'',
'',
},
dots_ellipsis = { -- simpleDots
'. ',
'.. ',
'...',
' ',
},
dots_scrolling = { -- simpleDotsScrolling
'. ',
'.. ',
'...',
' ..',
' .',
' ',
},
star = {
'',
'',
'',
'',
'',
'',
},
flip = {
'_',
'_',
'_',
'-',
'`',
'`',
"'",
'´',
'-',
'_',
'_',
'_',
},
hamburger = {
'',
'',
'',
},
grow_vertical = { -- growVertical
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
},
grow_horizontal = { -- growHorizontal
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
},
noise = {
'',
'',
'',
},
dots_bounce = { -- bounce
'',
'',
'',
'',
},
triangle = {
'',
'',
'',
'',
},
arc = {
'',
'',
'',
'',
'',
'',
},
circle = {
'',
'',
'',
},
square_corners = { -- squareCorners
'',
'',
'',
'',
},
circle_quarters = { -- circleQuarters
'',
'',
'',
'',
},
circle_halves = { -- circleHalves
'',
'',
'',
'',
},
dots_toggle = { -- toggle
'',
'',
},
box_toggle = { -- toggle2
'',
'',
},
arrow = {
'',
'',
'',
'',
'',
'',
'',
'',
},
clock = {
'🕛 ',
'🕐 ',
'🕑 ',
'🕒 ',
'🕓 ',
'🕔 ',
'🕕 ',
'🕖 ',
'🕗 ',
'🕘 ',
'🕙 ',
'🕚 ',
},
earth = {
'🌍 ',
'🌎 ',
'🌏 ',
},
moon = {
'🌑 ',
'🌒 ',
'🌓 ',
'🌔 ',
'🌕 ',
'🌖 ',
'🌗 ',
'🌘 ',
},
dots_pulse = { -- point
'∙∙∙',
'●∙∙',
'∙●∙',
'∙∙●',
'∙∙∙',
},
fistBump = {
'🤜    🤛 ',
'🤜    🤛 ',
'🤜    🤛 ',
' 🤜  🤛  ',
'  🤜🤛   ',
' 🤜✨🤛   ',
'🤜 ✨ 🤛  ',
},
monkey = {
'🙈 ',
'🙈 ',
'🙉 ',
'🙊 '
},
soccerHeader = {
' 🧑⚽️ 🧑 ',
'🧑 ⚽️ 🧑 ',
'🧑 ⚽️ 🧑 ',
'🧑 ⚽️ 🧑 ',
'🧑 ⚽️ 🧑 ',
'🧑 ⚽️ 🧑 ',
'🧑 ⚽️🧑 ',
'🧑 ⚽️ 🧑 ',
'🧑 ⚽️ 🧑 ',
'🧑 ⚽️ 🧑 ',
'🧑 ⚽️ 🧑 ',
'🧑 ⚽️ 🧑 ',
},
weather = {
'☀️ ',
'☀️ ',
'☀️ ',
'🌤 ',
'⛅️ ',
'🌥 ',
'☁️ ',
'🌧 ',
'🌨 ',
'🌧 ',
'🌨 ',
'🌧 ',
'🌨 ',
'',
'🌨 ',
'🌧 ',
'🌨 ',
'☁️ ',
'🌥 ',
'⛅️ ',
'🌤 ',
'☀️ ',
'☀️ ',
},
speaker = {
'🔈 ',
'🔉 ',
'🔊 ',
'🔉 ',
},
smiley = {
'😄 ',
'😝 ',
},
toggle = {
'',
''
},
toggle10 = {
'',
'',
''
},
toggle11 = {
'',
''
},
toggle12 = {
'',
''
},
toggle13 = {
'=',
'*',
'-'
},
toggle2 = {
'',
''
},
toggle3 = {
'',
''
},
toggle4 = {
'',
'',
'',
''
},
toggle5 = {
'',
''
},
toggle6 = {
'',
''
},
toggle7 = {
'',
'⦿'
},
toggle8 = {
'',
''
},
toggle9 = {
'',
''
},
star = {
'',
'',
'',
'',
'',
''
},
star2 = {
'+',
'x',
'*'
},
orangeBluePulse = {
'🔸 ',
'🔶 ',
'🟠 ',
'🟠 ',
'🔶 ',
'🔹 ',
'🔷 ',
'🔵 ',
'🔵 ',
'🔷 ',
},
orangePulse = {
'🔸 ',
'🔶 ',
'🟠 ',
'🟠 ',
'🔶 '
},
mindblown = {
'😐 ',
'😐 ',
'😮 ',
'😮 ',
'😦 ',
'😦 ',
'😧 ',
'😧 ',
'🤯 ',
'💥 ',
'',
'  ',
'  ',
'  ',
},
hearts = {
'💛 ',
'💙 ',
'💜 ',
'💚 ',
'❤️ '
},
fingerDance = {
'🤘 ',
'🤟 ',
'🖖 ',
'',
'🤚 ',
'👆 '
},
christmas = {
'🌲',
'🎄'
},
circleHalves = {
'',
'',
'',
''
},
bouncingBall = {
'( ● )',
'( ● )',
'( ● )',
'( ● )',
'( ●)',
'( ● )',
'( ● )',
'( ● )',
'( ● )',
'(● )',
},
bluePulse = {
'🔹 ',
'🔷 ',
'🔵 ',
'🔵 ',
'🔷 '
},
betaWave = {
'ρββββββ',
'βρβββββ',
'ββρββββ',
'βββρβββ',
'ββββρββ',
'βββββρβ',
'ββββββρ',
},
}

276
lua/Trans/ui/spinner.lua Normal file
View File

@ -0,0 +1,276 @@
-- Spinners adapted from: https://github.com/sindresorhus/cli-spinners
--
-- Some designs' names are made more descriptive; differences noted in comments.
-- Other designs are omitted for brevity.
--
-- You may want to adjust spinner_rate according to the number of frames of your
-- chosen spinner.
-- MIT License
--
-- Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
--
-- Permission is hereby granted, free of charge, to any person obtaining a copy
-- of this software and associated documentation files (the "Software"), to deal
-- in the Software without restriction, including without limitation the rights
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-- copies of the Software, and to permit persons to whom the Software is
-- furnished to do so, subject to the following conditions:
--
-- The above copyright notice and this permission notice shall be included in
-- all copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-- SOFTWARE.
return {
dots = {
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
},
dots_negative = { -- dots2
"",
"",
"",
"",
"",
"",
"",
"",
},
dots_snake = { -- dots3
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
},
dots_footsteps = { -- dots10
"",
"",
"",
"",
"",
"",
"",
},
dots_hop = { -- dots11
"",
"",
"",
"",
"",
"",
"",
"",
},
line = {
"-",
"\\",
"|",
"/",
},
pipe = {
"",
"",
"",
"",
"",
"",
"",
"",
},
dots_ellipsis = { -- simpleDots
". ",
".. ",
"...",
" ",
},
dots_scrolling = { -- simpleDotsScrolling
". ",
".. ",
"...",
" ..",
" .",
" ",
},
star = {
"",
"",
"",
"",
"",
"",
},
flip = {
"_",
"_",
"_",
"-",
"`",
"`",
"'",
"´",
"-",
"_",
"_",
"_",
},
hamburger = {
"",
"",
"",
},
grow_vertical = { -- growVertical
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
},
grow_horizontal = { -- growHorizontal
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
},
noise = {
"",
"",
"",
},
dots_bounce = { -- bounce
"",
"",
"",
"",
},
triangle = {
"",
"",
"",
"",
},
arc = {
"",
"",
"",
"",
"",
"",
},
circle = {
"",
"",
"",
},
square_corners = { -- squareCorners
"",
"",
"",
"",
},
circle_quarters = { -- circleQuarters
"",
"",
"",
"",
},
circle_halves = { -- circleHalves
"",
"",
"",
"",
},
dots_toggle = { -- toggle
"",
"",
},
box_toggle = { -- toggle2
"",
"",
},
arrow = {
"",
"",
"",
"",
"",
"",
"",
"",
},
clock = {
"🕛 ",
"🕐 ",
"🕑 ",
"🕒 ",
"🕓 ",
"🕔 ",
"🕕 ",
"🕖 ",
"🕗 ",
"🕘 ",
"🕙 ",
"🕚 ",
},
earth = {
"🌍 ",
"🌎 ",
"🌏 ",
},
moon = {
"🌑 ",
"🌒 ",
"🌓 ",
"🌔 ",
"🌕 ",
"🌖 ",
"🌗 ",
"🌘 ",
},
dots_pulse = { -- point
"∙∙∙",
"●∙∙",
"∙●∙",
"∙∙●",
"∙∙∙",
},
}

View File

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

89
lua/Trans/util/curl.lua Normal file
View File

@ -0,0 +1,89 @@
--- 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

@ -0,0 +1,48 @@
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,7 +1,3 @@
---@class TransUtil
---@field md5 TransUtilMd5
---@class TransUtilMd5
local md5 = {}
-- local md5 = {
-- _VERSION = "md5.lua 1.1.0",

View File

@ -1,101 +0,0 @@
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
local item = (function()
---@class TransItem : TransNode
local mt = {
---@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,
}
mt.__index = mt
---Basic item node
---@param tuple {[1]: string, [2]: string?}
---@return TransItem
return function(tuple)
return setmetatable(tuple, mt)
end
end)()
local text = (function()
---@class TransText : TransNode
---@field step string
---@field nodes TransNode[]
local mt = {
---@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,
}
mt.__index = mt
---@param nodes {[number]: TransNode, step: string?}
---@return TransText
return function(nodes)
return setmetatable({
[1] = table.concat(util.list_fields(nodes, 1), nodes.step),
step = nodes.step,
nodes = nodes,
}, mt)
end
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,
}

120
lua/Trans/view/float.lua Normal file
View File

@ -0,0 +1,120 @@
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

384
lua/Trans/view/hover.lua Normal file
View File

@ -0,0 +1,384 @@
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

224
lua/Trans/window.lua Normal file
View File

@ -0,0 +1,224 @@
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

View File

@ -1,144 +0,0 @@
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))

View File

@ -1,19 +0,0 @@
_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'
string.width = api.nvim_strwidth
---@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

View File

@ -1,92 +0,0 @@
---@diagnostic disable: param-type-mismatch
require 'test.setup'
local util = Trans.util
describe('util.display_height', function()
it('can calculate the height of lines when window with wrap option', function()
local lines = {
'1234567890',
'1234567890',
'1234567890',
'1234567890',
'1234567890',
'1234567890',
'1234567890',
'1234567890',
'1234567890',
}
assert.are.equal(#lines, util.display_height(lines, 10))
assert.are.equal(#lines, util.display_height(lines, 11))
assert.are.equal(2 * #lines, util.display_height(lines, 9))
-- Unicode width test
local u_lines = {
'12345678👍', -- 10
'あうえお', -- 8
'𠮷野い𠮷家野家家', -- 16
'👍👍👍お家', -- 10
}
assert.are.equal(4, util.display_height(u_lines, 20))
assert.are.equal(4, util.display_height(u_lines, 16))
assert.are.equal(5, util.display_height(u_lines, 10))
assert.are.equal(7, util.display_height(u_lines, 8))
assert.are.equal(9, util.display_height(u_lines, 7))
end)
end)
describe('util.display_width', function()
it('can calculate the max width of lines', function()
local lines = {
'1234567890',
'123456789',
'12345678',
'1234567',
'123456',
'12345',
'1234',
'123',
'12',
'1',
}
assert.are.equal(10, util.display_width(lines))
-- Unicode width test
local u_lines = {
'12345678👍', -- 10
'あうえお', -- 8
'𠮷野い𠮷家野家家', -- 16
'👍👍👍お家', -- 10
}
assert.are.equal(16, util.display_width(u_lines))
end)
end)
describe('util.center', function()
it('will return the node if its width more than width', function()
local node = i { '1234567890' }
assert.are.same(node, util.center(node, 9))
end)
it('will auto padding space', function()
local node = i { '1234567890' }
assert.are.same(i { (' '):rep(2) .. '1234567890' }, util.center(node, 15))
end)
end)
describe('util.is_word', function()
it('can detect word', function()
for test, value in pairs {
['あうえお'] = false,
['hello'] = true,
[' hello'] = false,
['hello world'] = false,
['test_cool'] = false,
} do
assert.are.same(util.is_word(test), value)
end
end)
end)

View File

@ -1,100 +0,0 @@
require 'test.setup'
describe('window', with_buffer(function(buffer)
local window
before_each(function()
buffer:wipe()
window = Trans.window.new {
buffer = buffer,
win_opts = {
col = 1,
row = 1,
width = 1,
height = 1,
relative = 'editor',
},
}
window:set('wrap', false)
end)
after_each(function()
window:try_close()
end)
it('can work well when no pass animation table', function()
window:open()
assert.is_true(api.nvim_win_is_valid(window.winid))
end)
describe('smooth_expand', function()
it('can work well when no pass animation table', function()
for field, values in pairs {
width = {
10,
6,
8,
5,
},
height = {
10,
6,
3,
},
} do
for _, value in ipairs(values) do
window:smooth_expand { field = field, to = value }
assert.are.same(value, window[field](window))
end
end
end)
it("don't change window wrap option", function()
window:smooth_expand { field = 'width', to = 10 }
assert.is_false(window:option 'wrap')
window:set('wrap', true)
window:smooth_expand { field = 'width', to = 10 }
assert.is_true(window:option 'wrap')
window:smooth_expand { field = 'height', to = 10 }
assert.is_true(window:option 'wrap')
end)
end)
it("resize() don't change window wrap option", function()
window:resize { width = 10, height = 10 }
assert.is_false(window:option 'wrap')
window:set('wrap', true)
window:resize { width = 5, height = 5 }
assert.is_true(window:option 'wrap')
end)
it('adjust_height() can auto adjust window height to buffer display height', function()
for idx, content in ipairs {
'cool',
'co10',
'家👍',
'👍ol',
'cあl',
'家野',
} do
buffer[idx] = content
end
local max_height = vim.o.lines - 2
for width, expect in ipairs {
[2] = 12,
[3] = 12,
[4] = 6,
[5] = 6,
} do
window:smooth_expand { field = 'width', to = width }
window:adjust_height()
assert.are.same(math.min(expect, max_height), window:height())
end
end)
end))

View File

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

View File

@ -1,39 +1,12 @@
local api, fn = vim.api, vim.fn
--- INFO :Define plugin command
local Trans = require 'Trans'
local M = require("Trans")
local api = vim.api
local command = api.nvim_create_user_command
command('Translate', function() Trans.translate() end,
{ desc = '󰊿 Translate cursor word' })
command('TranslateInput', function() Trans.translate { mode = 'i' } end,
{ desc = '󰊿 Translate input word' })
command('Translate', function() M.translate() end, { desc = ' 单词翻译', })
command('TranslateInput', function() M.translate('i') end, { desc = ' 搜索翻译', })
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 = ' Auto play' })
string.width = api.nvim_strwidth
local system = Trans.system
local f =
(vim.fn.has 'wsl' == 1 or system == 'win') and
'powershell.exe -Command "Add-Type -AssemblyName System.speech;(New-Object System.Speech.Synthesis.SpeechSynthesizer).Speak(\\\"%s\\\")"' or
system == 'mac' and 'say %q' or
system == 'termux' and 'termux-tts-speak %q' or
system == 'linux' and 'echo %q | festival --tts' or
error 'Unsupported system'
string.play = function(self)
---@diagnostic disable-next-line: param-type-mismatch
local s = string.gsub(self, '\"', ' ')
fn.jobstart(f:format(s))
local word = M.get_word(api.nvim_get_mode().mode)
if word ~= '' and word:isEn() then
word:play()
end
end, { desc = ' 自动发音' })

View File

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

5
tts/package.json Normal file
View File

@ -0,0 +1,5 @@
{
"dependencies": {
"say": "^0.16.0"
}
}

4
tts/say.js Normal file
View File

@ -0,0 +1,4 @@
const say = require('say')
// console.log(word)
say.speak(process.argv.slice(2))