30 Commits

Author SHA1 Message Date
82779cc299 refactor: backup 2023-09-08 22:44:23 +08:00
eb68b8bb95 refactor: rewrite setup and conf module 2023-07-24 22:28:09 +08:00
073e8667b2 refactor: redirect Trans.conf 2023-07-13 10:15:38 +08:00
c2e56f7769 chore: backup progress, begin to rewrite version 3 2023-07-10 21:21:04 +08:00
5845a40d94 chore: add MIT License 2023-05-14 10:53:39 +08:00
f17171d28b fix: query strategy 2023-05-13 20:04:19 +08:00
ab212fefe1 chore: fix check health termux binary dependencies error 2023-05-13 20:03:57 +08:00
586cd6ff08 fix: system checker mistake 2023-05-13 19:36:40 +08:00
b5e626a88c Merge pull request #34 from Xiao-M0/v2
feat: add win wsl, remove python and nodejs dependencies
2023-05-13 19:07:36 +08:00
e993dc12be fix: get_select get_lines 2023-05-13 18:16:28 +08:00
871cc1e48a refactor: wsl 2023-05-13 18:13:32 +08:00
6e02f3b1ba fix: update exchange_map 2023-05-12 20:39:46 +08:00
377c84c146 fix: double quote error 2023-05-12 20:27:39 +08:00
ba08913b1e feat: get_lines 2023-04-25 13:48:52 +08:00
854bce7b5a chore: remove useless code 2023-04-24 20:58:56 +08:00
a48caf51e1 remove: node 2023-04-24 19:57:33 +08:00
24a838646d feat: add win wsl 2023-04-24 18:48:36 +08:00
156e03306a Merge pull request #33 from Xiao-M0/v2
fix: win separator
2023-04-24 13:04:43 +08:00
0a1b6cf742 feat: say.py 2023-04-24 13:02:48 +08:00
758b9c0b33 fix: win separator 2023-04-24 12:42:59 +08:00
3faab735fb chore: make hover window focusable and update docs 2023-04-16 16:38:39 +08:00
335c5079cc docs: update README 2023-04-08 21:46:29 +08:00
38b8e20729 chore: close window and buffer more safely 2023-04-07 19:05:24 +08:00
fcde85544a feat: use a non-invasive keymap define method 2023-04-06 13:38:19 +08:00
2ae2effecc chore: more spinner style support 2023-04-04 16:19:06 +08:00
da6e717f2c feat: use a non-invasive keymap define method 2023-04-02 18:02:21 +08:00
576f1eb66a feat: add termux tts support 2023-04-02 13:34:54 +08:00
9357574b5c chore: sync 2023-04-01 09:59:09 +08:00
987e1a3341 test: add simple window unit test 2023-03-31 12:51:11 +08:00
b27bc4ed26 fix: add util unit test and fix util.center mistake 2023-03-31 01:04:36 +08:00
36 changed files with 1560 additions and 824 deletions

21
LICENCE Normal file
View File

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

250
README.md
View File

@ -2,51 +2,48 @@
<!--toc:start--> <!--toc:start-->
- [Trans.nvim](#transnvim) - [注意: 当前分支目前没有发布, README.md 的描述并不准确, 遇到问题请切换到 `main`分支或者联系我](#注意-当前分支目前没有发布-readmemd-的描述并不准确-遇到问题请切换到-main分支或者联系我) - [Trans.nvim](#transnvim)
- [特点](#特点) - [特点](#特点)
- [屏幕截图](#屏幕截图) - [屏幕截图](#屏幕截图)
- [演示](#演示) - [演示](#演示)
- [离线查询](#离线查询) - [离线查询](#离线查询)
- [\*在线查询\*\* (有道)](#在线查询-有道) - [在线查询演示 (有道)](#在线查询演示-有道)
- [主题](#主题) - [主题](#主题)
- [安装](#安装) - [安装](#安装)
- [配置](#配置) - [配置](#配置)
- [快捷键绑定](#快捷键绑定)
- [高亮组](#高亮组) - [高亮组](#高亮组)
- [声明](#声明) - [声明](#声明)
- [感谢](#感谢) - [感谢](#感谢)
- [贡献](#贡献) - [贡献](#贡献)
- [从 v1 (main)分支迁移](#从-v1-main分支迁移)
- [待办 (画大饼)](#待办-画大饼) - [待办 (画大饼)](#待办-画大饼)
- [项目情况](#项目情况) - [项目情况](#项目情况)
<!--toc:end--> <!--toc:end-->
### 注意: 当前分支目前没有发布, README.md 的描述并不准确, 遇到问题请切换到 `main`分支或者联系我 > **插件默认词库的路径为插件目录**
例如: `lazy` 用户应该在 `$HOME/.local/share/nvim/lazy/Trans.nvim`
## 特点 ## 特点
- 使用纯 lua 编写, 速度极快 - `使用纯 lua 编写`
> `Lazy.nvim`的记录: <font color="#0099FF">`➜  Trans.nvim 0.82ms`</font>
- **可以定义快捷键读英文单词**
> 见 wiki
- 大部分功能可以自定义: - 大部分功能可以自定义:
- 高亮 - 🔍 高亮
- 悬浮大小 - 👀 悬浮大小
- 排版顺序 - 📜 排版顺序
- 弹窗大小 - 💬 弹窗大小
- `舒服窗口动画` - 🎉 舒服窗口动画
- etc (更多可以查看[配置](#配置)) - 更多可以查看[配置](#配置)
- **完全离线** 的单词翻译体验 (可能后面会支持在线翻译) - `离线``在线`翻译的支持
- 支持显示: - 支持显示:
- 柯林斯星级 - 🌟 柯林斯星级
- 牛津 3000 词汇 - 📚 牛津 3000 词汇
- 中文翻译 - 🇨🇳 中文翻译
- 英文翻译 (不是英译中, 而是用英文解释) - 🇺🇸 英文翻译 (不是英译中, 而是用英文解释)
- 词根 - 🌱 词根
- etc - etc
- 舒服的排版和`动画` - 支持`平滑动画`
- 支持 `normal``visual`模式 - 支持 `normal``visual`模式
> <font color='#FF9900'>不支持 visual-block mode</font> > <font color='#FF9900'>不支持 visual-block mode</font>
- 本地词库单词量: `430w` - 本地词库单词量: `430w`
@ -61,13 +58,13 @@
https://user-images.githubusercontent.com/107862700/226175984-1a95bea7-8d66-450e-87e1-ba9c91c37ab8.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 https://user-images.githubusercontent.com/107862700/226176106-c2962dd3-d66c-499c-b44a-1f471b79fe38.mp4
**使用在线查询需要配置相应的 app_id 和 app_passwd** **使用在线查询需要配置相应的 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) 配置说明见: [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)
### 主题 ### 主题
@ -139,6 +136,7 @@ use {
```lua ```lua
{ {
"JuanZoran/Trans.nvim", "JuanZoran/Trans.nvim",
build = function () require'Trans'.install() end,
keys = { keys = {
-- 可以换成其他你想映射的键 -- 可以换成其他你想映射的键
{ 'mm', mode = { 'n', 'x' }, '<Cmd>Translate<CR>', desc = ' Translate' }, { 'mm', mode = { 'n', 'x' }, '<Cmd>Translate<CR>', desc = ' Translate' },
@ -157,40 +155,32 @@ use {
<font color="#FF9900">**注意事项**: </font> <font color="#FF9900">**注意事项**: </font>
- `install.sh`
- 使用了 `wget`下载词库, 安装请确保你的环境变量中存在 wget
- install.sh 下载后会自动将词库解压, 并移动到 `$HOME/.vim/dict`文件夹下
- 目前仅在 `Ubuntu22.04`的环境下测试通过
> 如果上述条件不符合, 请删掉 `run = 'install.sh'`部分, 考虑手动安装词库
> 如果上述条件满足, 仍出现问题, 欢迎在 issue 里向我反馈,我会及时尝试解决
- 下载词典的过程中, 需要能够 `流畅的访问github下载` - 下载词典的过程中, 需要能够 `流畅的访问github下载`
> 词库文件压缩包大小为: **281M** 如果下载出现问题, 正常是会自动下载
> 解压缩后的大小大概为: 1.2G
- 安装后如果不能正常运行, 请尝试检查一下问题: > 词库文件压缩包大小为: **281M**
> 解压缩后的大小大概为: **1.2G**
- 本机是否已经安装了 `sqlite3` - 安装后如果不能正常运行, 清尝试运行 `checkhealth Trans`
> Linux 下安装:
> `sudo pacman -S sqlite # Arch`
> `sudo apt-get install sqlite3 libsqlite3-dev # Ubuntu`
> **尝试运行 `checkhealth Trans`** - **`auto_play`** 的使用:
- **`auto_play`** 使用步骤: - `Linux` 需要安装`festival`
> linux 只需要安装`festival` > `sudo apt-get install festival festvox-kallpc16k`
> sudo apt-get install festival festvox-kallpc16k
> **_如果你想要设置音色发音可以访问:_** [Festival 官方](https://www.cstr.ed.ac.uk/projects/festival/morevoices.html)
> 可以选择英音、美音、男声、女声
> 其他操作系统 **如果你想要设置音色,发音可以访问:** [Festival 官方](https://www.cstr.ed.ac.uk/projects/festival/morevoices.html)
可以选择英音、美音、男声、女声
- 需要确保安装`nodejs` - `Termux` 需要安装`termux-api`
- 进入插件的`tts`目录运行`npm install`
> 如果`install`运行正常则自动安装,如果安装失败,请尝试手动安装 - `Mac` 使用系统的`say`命令
- `Windows` 使用 `nodejs`的 say 模块, 如果你有更好的方案欢迎提供 PR
- 需要确保安装了`nodejs`
- 进入插件的`tts`目录运行`npm install`
> 如果`install`运行正常则自动安装,如果安装失败,请尝试手动安装
- `title`的配置,只对`neovim 0.9+`版本有效 - `title`的配置,只对`neovim 0.9+`版本有效
@ -244,12 +234,16 @@ use {
## 配置 ## 配置
详细见**wiki**: [配置说明](https://github.com/JuanZoran/Trans.nvim/wiki/%E9%85%8D%E7%BD%AE) 详细见**wiki**: [基本配置说明](https://github.com/JuanZoran/Trans.nvim/wiki/%E9%85%8D%E7%BD%AE)
<details>
<summary>默认配置</summary>
```lua ```lua
require 'Trans'.setup { default_conf = {
---@type string the directory for database file and password file ---@type string the directory for database file and password file
dir = require 'Trans'.plugin_dir, dir = require 'Trans'.plugin_dir,
debug = true,
---@type 'default' | 'dracula' | 'tokyonight' global Trans theme [see lua/Trans/style/theme.lua] ---@type 'default' | 'dracula' | 'tokyonight' global Trans theme [see lua/Trans/style/theme.lua]
theme = 'default', -- default | tokyonight | dracula theme = 'default', -- default | tokyonight | dracula
strategy = { strategy = {
@ -294,11 +288,13 @@ require 'Trans'.setup {
split_width = 60, split_width = 60,
padding = 10, -- padding for hover window width padding = 10, -- padding for hover window width
keymaps = { keymaps = {
pageup = '[[', -- INFO : No default keymaps anymore, please set it yourself
pagedown = ']]', -- pageup = '<C-u>',
pin = '<leader>[', -- pagedown = '<C-d>',
close = '<leader>]', -- pin = '<leader>[',
toggle_entry = '<leader>;', -- close = '<leader>]',
-- toggle_entry = '<leader>;',
-- play = '_', -- Deprecated -- play = '_', -- Deprecated
}, },
---@type string[] auto close events ---@type string[] auto close events
@ -349,6 +345,8 @@ require 'Trans'.setup {
} }
``` ```
</details>
## 快捷键绑定 ## 快捷键绑定
**示例:** **示例:**
@ -361,66 +359,90 @@ vim.keymap.set({'n', 'x'}, 'mm', '<Cmd>Translate<CR>')
vim.keymap.set({'n', 'x'}, 'mk', '<Cmd>TransPlay<CR>') -- 自动发音选中或者光标下的单词 vim.keymap.set({'n', 'x'}, 'mk', '<Cmd>TransPlay<CR>') -- 自动发音选中或者光标下的单词
``` ```
## 高亮组 **窗口快捷键**
> 默认定义
```lua ```lua
{ require 'Trans'.setup {
TransWord = { frontend = {
fg = '#7ee787', hover = {
bold = true, keymaps = {
-- pageup = 'whatever you want',
-- pagedown = 'whatever you want',
-- pin = 'whatever you want',
-- close = 'whatever you want',
-- toggle_entry = 'whatever you want',
},
}, },
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',
}
} }
}
``` ```
> 当窗口没有打开的时候, 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'
}
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 = {
link = 'MoreMsg',
}
```
</details>
## 声明 ## 声明
- 本插件词典基于[ECDICT](https://github.com/skywind3000/ECDICT) - 本插件词典基于[ECDICT](https://github.com/skywind3000/ECDICT)
@ -436,6 +458,10 @@ vim.keymap.set({'n', 'x'}, 'mk', '<Cmd>TransPlay<CR>') -- 自动发音选中或
> 更新比较频繁, 文档先鸽了 > 更新比较频繁, 文档先鸽了
> 如果你想要参加这个项目, 可以提 issue, 我会把文档补齐 > 如果你想要参加这个项目, 可以提 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] 快捷键定义

View File

@ -15,4 +15,9 @@
- [ ] Unlimit width for sentence - [ ] Unlimit width for sentence
已知问题:
1. 缓存了的单词, 无法使用toggle_entry 进入页面
2. 加载配置需要输入所有表的key
- default_strategy can't deal with table correctly - default_strategy can't deal with table correctly

View File

@ -1,18 +1,13 @@
---@class Baidu: TransOnlineBackend ---@class Baidu: TransBackendOnline
---@field uri string api uri ---@field conf { app_id: string, app_passwd: string }
---@field salt string
---@field app_id string
---@field app_passwd string
---@field disable boolean
local M = { local M = {
uri = 'https://fanyi-api.baidu.com/api/trans/vip/translate', name = 'baidu',
salt = tostring(math.random(bit.lshift(1, 15))), display_text = '百度',
name = 'baidu', uri = 'https://fanyi-api.baidu.com/api/trans/vip/translate',
name_zh = '百度', salt = tostring(math.random(bit.lshift(1, 15))),
method = 'get', method = 'get',
} }
local Trans = require 'Trans' local Trans = require 'Trans'
---@class BaiduQuery ---@class BaiduQuery
@ -27,14 +22,17 @@ local Trans = require 'Trans'
---@param data TransData ---@param data TransData
---@return BaiduQuery ---@return BaiduQuery
function M.get_query(data) function M.get_query(data)
local tmp = M.app_id .. data.str .. M.salt .. M.app_passwd local m_conf = M.conf
assert(m_conf, 'Load Baidu config failed')
local tmp = m_conf.app_id .. data.str .. M.salt .. m_conf.app_passwd
local sign = Trans.util.md5.sumhexa(tmp) local sign = Trans.util.md5.sumhexa(tmp)
return { return {
q = data.str, q = data.str,
from = data.from, from = data.from,
to = data.to, to = data.to,
appid = M.app_id, appid = m_conf.app_id,
salt = M.salt, salt = M.salt,
sign = sign, sign = sign,
} }
@ -57,14 +55,31 @@ function M.formatter(body, data)
} }
end end
---@class TransBackend
---@field baidu Baidu
return M 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
-- 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"}]}', -- body = '{"from":"en","to":"zh","trans_result":[{"src":"require","dst":"\\u8981\\u6c42"}]}',
-- exit = 0, -- exit = 0,

View File

@ -1,4 +1,191 @@
local Trans = require 'Trans' local Trans = require 'Trans'
if false then
-- local dict = db:open(Trans.conf.dir .. Trans.separator .. 'ultimate.db')
local db = require 'sqlite.db'
local conf = Trans.loader.conf
local dict = db:open(conf.dict)
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
function M.query(data)
if data.is_word == false or data.from == 'zh' then
return
end
local res = dict:select(conf.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 {
name = 'offline',
name_zh = '本地',
no_wait = true,
}
end
local db = require 'sqlite.db' local db = require 'sqlite.db'
local path = Trans.conf.dir .. Trans.separator .. 'ultimate.db' local path = Trans.conf.dir .. Trans.separator .. 'ultimate.db'
@ -18,8 +205,6 @@ local M = {
} }
---@param data any ---@param data any
---@return any
---@overload fun(TransData): TransResult
function M.query(data) function M.query(data)
if data.is_word == false or data.from == 'zh' then if data.is_word == false or data.from == 'zh' then
return return
@ -99,6 +284,8 @@ local formatter = {
['p'] = '过去式 ', ['p'] = '过去式 ',
['r'] = '比较级 ', ['r'] = '比较级 ',
['t'] = '最高级 ', ['t'] = '最高级 ',
['b'] = '比较级 ',
['z'] = '最高级 ',
['s'] = '复数 ', ['s'] = '复数 ',
['d'] = '过去分词 ', ['d'] = '过去分词 ',
['i'] = '现在分词 ', ['i'] = '现在分词 ',
@ -176,6 +363,4 @@ function M.formatter(res)
return res return res
end end
---@class TransBackends
---@field offline TransOfflineBackend
return M return M

View File

@ -1,15 +1,14 @@
---@class Youdao: TransOnlineBackend ---@class Youdao: TransBackendOnline
---@field uri string api uri ---@field uri string api uri
---@field salt string ---@field salt string
---@field app_id string
---@field app_passwd string
---@field disable boolean ---@field disable boolean
---@field conf { app_id: string, app_passwd: string }
local M = { local M = {
uri = 'https://openapi.youdao.com/api', uri = 'https://openapi.youdao.com/api',
salt = tostring(math.random(bit.lshift(1, 15))), name = 'youdao',
name = 'youdao', display_text = '有道',
name_zh = '有道', method = 'get',
method = 'get', salt = tostring(math.random(bit.lshift(1, 15))),
} }
---@class YoudaoQuery ---@class YoudaoQuery
@ -26,7 +25,7 @@ local M = {
---@return YoudaoQuery ---@return YoudaoQuery
function M.get_query(data) function M.get_query(data)
local str = data.str local str = data.str
local app_id = M.app_id local m_conf = M.conf
local salt = M.salt local salt = M.salt
local curtime = tostring(os.time()) local curtime = tostring(os.time())
@ -38,7 +37,7 @@ function M.get_query(data)
-- sign=sha256(应用ID+input+salt+curtime+应用密钥) 一二三四五六七八九十 -- sign=sha256(应用ID+input+salt+curtime+应用密钥) 一二三四五六七八九十
local hash = app_id .. input .. salt .. curtime .. M.app_passwd local hash = m_conf.app_id .. input .. salt .. curtime .. m_conf.app_passwd
local sign = vim.fn.sha256(hash) local sign = vim.fn.sha256(hash)
@ -47,7 +46,7 @@ function M.get_query(data)
to = data.from == 'zh' and 'en' or 'zh-CHS', to = data.from == 'zh' and 'en' or 'zh-CHS',
from = 'auto', from = 'auto',
signType = 'v3', signType = 'v3',
appKey = app_id, appKey = m_conf.app_id,
salt = M.salt, salt = M.salt,
curtime = curtime, curtime = curtime,
sign = sign, sign = sign,
@ -160,6 +159,26 @@ end
---@class TransBackend ---@class TransBackend
---@field youdao Youdao ---@field youdao Youdao
return M return M
-- INFO :Query Result Example -- INFO :Query Result Example
-- { -- {

View File

@ -0,0 +1,8 @@
local Trans = require'Trans'
---@class Trans
local M = {}
return M

View File

@ -1,27 +1,68 @@
local Trans = require 'Trans' local Trans = require 'Trans'
---@class TransBackend ---@class TransBackend
---@field no_wait? boolean whether need to wait for the result ---@field no_wait? boolean whether need to wait for the result
---@field all_name string[] @all backend name ---@field name string
---@field name string @backend name ---@field display_text string?
---@field name_zh string @backend name in Chinese ---@field conf table? @User specific config
---@class TransOnlineBackend: TransBackend
---@class TransBackendOnline: TransBackend
---@field uri string @request uri ---@field uri string @request uri
---@field method 'get' | 'post' @request method ---@field method 'get' | 'post' @request method
---@field formatter fun(body: table, data: TransData): TransResult|false|nil @formatter ---@field formatter fun(body: table, data: TransData): TransResult|false|nil transform response body to TransResult
---@field get_query fun(data: TransData): table<string, string> @get query ---@field get_query fun(data: TransData): table<string, any> @get query table
---@field header? table<string, string> | fun(data: TransData): table<string, string> @request header ---@field error_message? fun(errorCode) @get error message
---@field debug? fun(body: table?) @debug
-- -@field header table<string, string>|fun(data: TransData): table<string, string> @request header
local conf = Trans.conf ---@class TransBackendOffline: TransBackend
--- INFO :Parse online engine keys config file ---@field query fun(data: TransData)
local path = conf.dir .. '/Trans.json'
---@class TransBackendCore
local M = {
---@type table<string, TransBackend> backendname -> backend source
sources = {},
}
local m_util = {}
-- INFO :Template method for online query
---@param data TransData @data
---@param backend TransBackendOnline @backend
function M.do_query(data, backend)
local name = backend.name
local formatter = backend.formatter
-- 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
data.result[name] = false
return
end
data.result[name] = formatter(body, data)
end
Trans.curl[backend.method](backend.uri, {
query = backend.get_query(data),
callback = handle,
--- FIXME :
header = header,
})
end
-- TODO :Implement all of utility functions
M.util = m_util
-- INFO :Parse configuration file
local path = Trans.conf.dir .. '/Trans.json'
local file = io.open(path, 'r') local file = io.open(path, 'r')
local user_conf = {} local user_conf = {}
if file then if file then
local content = file:read '*a' local content = file:read '*a'
@ -29,33 +70,16 @@ if file then
file:close() file:close()
end end
-- WARNING : [Breaking change] 'Trans.json' should use json object instead of array
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 ---@class Trans
---@field backend TransBackends ---@field backend TransBackendCore
return setmetatable({ return setmetatable(M, {
all_name = all_name,
}, {
__index = function(self, name) __index = function(self, name)
---@type TransBackend ---@type TransBackend
local backend = require('Trans.backend.' .. name) local backend = require('Trans.backend.' .. name)
backend.conf = user_conf[name]
for key, value in pairs(user_conf[name] or {}) do self.sources[name] = backend
backend[key] = value
end
self[name] = backend
return backend return backend
end, end,
}) })

View File

@ -2,7 +2,7 @@ local api, fn = vim.api, vim.fn
---@class TransBuffer ---@class TransBuffer
---@field bufnr integer buffer handle ---@field bufnr integer buffer handle
---@field [number] string|TransNode|TransNode[] buffer[line] content ---@field [integer] string|TransNode|TransNode[] buffer[line] content
local buffer = {} local buffer = {}
-- INFO : corountine can't invoke C function -- INFO : corountine can't invoke C function
@ -37,7 +37,7 @@ end
---Destory buffer ---Destory buffer
function buffer:destroy() function buffer:destroy()
api.nvim_buf_delete(self.bufnr, { force = true }) pcall(api.nvim_buf_delete, self.bufnr, { force = true })
end end
---Set buffer load keymap ---Set buffer load keymap
@ -91,11 +91,7 @@ end
---@return integer ---@return integer
function buffer:line_count() function buffer:line_count()
local line_count = api.nvim_buf_line_count(self.bufnr) local line_count = api.nvim_buf_line_count(self.bufnr)
if line_count == 1 and self[1] == '' then return line_count == 1 and self[1] == '' and 0 or line_count
return 0
end
return line_count
end end
---Set line content ---Set line content
@ -151,11 +147,11 @@ buffer.__index = function(self, key)
end end
end end
buffer.__newindex = function(self, key, nodes) buffer.__newindex = function(self, key, values)
if type(key) == 'number' then if type(key) == 'number' then
self:setline(nodes, key) self:setline(values, key)
else else
rawset(self, key, nodes) rawset(self, key, values)
end end
end end

View File

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

View File

@ -1,53 +1,39 @@
local Trans = require 'Trans' local Trans = require 'Trans'
---@class TransData: TransDataOption
---@class TransData ---@field mode string @The mode of the str
---@field from string @Source language type ---@field from string @Source language type
---@field to string @Target language type ---@field to string @Target language type
---@field is_word boolean @Is the str a word
---@field str string @The original string ---@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 result table<string, TransResult|nil|false> @The result of the translation
---@field frontend TransFrontend ---@field frontend TransFrontend
---@field is_word? boolean @Is the str a word
---@field trace table<string, string> debug message ---@field trace table<string, string> debug message
---@field backends table<string, TransBackend> ---@field backends TransBackend[]
local M = {} local M = {}
M.__index = M M.__index = M
---TransData constructor ---TransData constructor
---@param opts table ---@param opts TransDataOption
---@return TransData ---@return TransData
function M.new(opts) function M.new(opts)
local mode = opts.mode
local str = opts.str
---@cast opts TransData
local mode = opts.mode
opts.result = {}
opts.trace = {}
local strategy = Trans.conf.strategy[mode] local strategy = Trans.conf.strategy[mode]
local data = setmetatable({
str = str,
mode = mode,
result = {},
trace = {},
}, M)
data.frontend = Trans.frontend[strategy.frontend].new() ---@cast opts TransData
data.backends = {} setmetatable(opts, M)
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) -- NOTE : whether should we use the default strategy
opts.frontend = Trans.frontend[strategy.frontend].new()
opts.backends = {}
return data return opts
end end
---@class TransResult ---@class TransResult
@ -67,12 +53,8 @@ end
---@return string? backend.name ---@return string? backend.name
function M:get_available_result() function M:get_available_result()
local result = self.result local result = self.result
if result['offline'] then return result['offline'], 'offline' end
for _, backend in ipairs(self.backends) do for _, backend in ipairs(self.backends) do
if result[backend.name] then if result[backend.name] then
---@diagnostic disable-next-line: return-type-mismatch
return result[backend.name], backend.name return result[backend.name], backend.name
end end
end end

8
lua/Trans/core/debug.lua Normal file
View File

@ -0,0 +1,8 @@
---@class Trans
---@field debug fun(message: string, level: number?)
return function (message, level)
level = level or vim.log.levels.INFO
-- TODO : custom messaage filter
vim.notify(message, level)
end

View File

@ -1,32 +1,65 @@
local Trans = require 'Trans' local Trans = require 'Trans'
local conf = Trans.conf
local frontend_opts = conf.frontend ---@class TransFrontendOpts
local default_frontend = {
auto_play = true,
query = 'fallback',
border = 'rounded',
title = vim.fn.has 'nvim-0.9' == 1 and {
{ '', 'TransTitleRound' },
{ ' Trans', 'TransTitle' },
{ '', 'TransTitleRound' },
} or nil, -- need nvim-0.9+
---@type {open: string | boolean, close: string | boolean, interval: integer} Window Animation
animation = {
open = 'slid', -- 'fold', 'slid'
close = 'slid',
interval = 12,
},
timeout = 2000, -- only for online backend query
}
if Trans.conf.frontend.default then
default_frontend = vim.tbl_extend('force', default_frontend, Trans.conf.frontend.default)
end
local function empty_method(method)
return function() error('Method [' .. method .. '] not implemented') end
end
---@class TransFrontend ---@class TransFrontend
---@field opts TransFrontendOpts ---@field opts? TransFrontendOpts options which user can set
---@field get_active_instance fun():TransFrontend? ---@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 wait fun(self: TransFrontend): fun(backend: TransBackend): boolean Update wait status
---@field execute fun(action: string) @Execute action for frontend instance ---@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** ---@field setup? fun() @Setup method for frontend [optional] **NOTE: This method will be called when frontend is first used**
local M = {
---@type fun() @Fallback method when no result
fallback = empty_method 'fallback',
---@type fun(self: TransFrontend, data: TransData) @render backend result
process = empty_method 'process',
}
---@class Trans ---@class Trans
---@field frontend TransFrontend ---@field frontend TransFrontend
return setmetatable({}, { return setmetatable(M, {
__index = function(self, name) __index = function(self, name)
local opts = vim.tbl_extend('keep', frontend_opts[name] or {}, frontend_opts.default)
---@type TransFrontend ---@type TransFrontend
local frontend = require('Trans.frontend.' .. name) local frontend = require('Trans.frontend.' .. name)
frontend.opts = opts frontend.opts =
self[name] = frontend vim.tbl_extend('force', frontend.opts or {}, default_frontend, Trans.conf.frontend[name])
if frontend.setup then if frontend.setup then
frontend.setup() frontend.setup()
end end
rawset(self, name, frontend)
return frontend return frontend
end, end,
}) })

19
lua/Trans/core/helper.lua Normal file
View File

@ -0,0 +1,19 @@
local fn, api = vim.fn, vim.api
local Trans = require 'Trans'
---@class TransHelper for Trans module dev
local M = {}
---Get abs_path of file
---@param path string[]
---@param is_dir boolean? [default]: false
---@return string @Generated path
function M.relative_path(path, is_dir)
return Trans.plugin_dir .. table.concat(path, Trans.separator) .. (is_dir and Trans.separator or '')
end
return M

7
lua/Trans/core/init.lua Normal file
View File

@ -0,0 +1,7 @@
-- Return Core module
local M = {}
return M

View File

@ -40,7 +40,6 @@ return function()
vim.notify(debug_message, vim.log.ERROR) vim.notify(debug_message, vim.log.ERROR)
end end
Trans.curl.get(uri, { Trans.curl.get(uri, {
output = zip, output = zip,
callback = handle, callback = handle,
@ -51,7 +50,7 @@ return function()
vim.notify(message, vim.log.levels.INFO) vim.notify(message, vim.log.levels.INFO)
-- INFO : Install tts dependencies -- INFO : Install tts dependencies
if fn.has 'linux' == 0 and fn.has 'mac' == 0 then if Trans.system == 'win' then
os.execute 'cd ./tts && npm install' os.execute 'cd ./tts && npm install'
end end
end end

View File

@ -1,44 +1,31 @@
---@class Trans
local Trans = require 'Trans' local Trans = require 'Trans'
local function set_strategy_opts(conf) ---@alias TransMode 'visual' 'input'
local all_backends = Trans.backend.all_name local default_strategy = {
local g_strategy = conf.strategy frontend = 'hover',
backend = {
'offline',
-- 'youdao',
-- 'baidu',
},
}
local function parse_backend(backend) Trans.conf = {
if type(backend) == 'string' then ---@type string the directory for database file and password file
return backend == '*' and all_backends or { backend } dir = require 'Trans'.plugin_dir,
end ---@type 'default' | 'dracula' | 'tokyonight' global Trans theme [@see lua/Trans/style/theme.lua]
theme = 'default',
---@type table<TransMode, { frontend:string, backend:string | string[] }> fallback strategy for mode
-- input = {
-- visual = {
-- ...
strategy = vim.defaulttable(function()
return setmetatable({}, default_strategy)
end),
frontend = {},
}
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 ---@class Trans
@ -48,8 +35,12 @@ return function(opts)
Trans.conf = vim.tbl_deep_extend('force', Trans.conf, opts) Trans.conf = vim.tbl_deep_extend('force', Trans.conf, opts)
end end
local conf = Trans.conf local conf = Trans.conf
conf.dir = vim.fn.expand(conf.dir) conf.dir = vim.fn.expand(conf.dir)
set_strategy_opts(conf) -- INFO : set highlight
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 end

View File

@ -1,94 +1,45 @@
local Trans = require 'Trans' local Trans = require 'Trans'
local util = Trans.util
local function init_opts(opts) local function process(opts)
opts = opts or {} opts = opts or {}
opts.mode = opts.mode or vim.fn.mode() opts.mode = opts.mode or vim.fn.mode()
opts.str = util.get_str(opts.mode) local str = Trans.util.get_str(opts.mode)
return opts opts.str = str
end
---To Do Online Query if not str or str == '' then
---@param data TransData @data Trans.debug 'No string to translate'
---@param backend TransOnlineBackend @backend return
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 end
Trans.curl[method](uri, {
query = query,
callback = handle,
header = header,
})
-- Hook ?
end
---@type table<string, fun(data: TransData):boolean> if opts.from == nil and opts.to == nil then
local strategy = { -- INFO : Default support [zh -> en] or [en -> zh]
fallback = function(data) if Trans.util.is_english(str) then
local result = data.result opts.from = 'en'
Trans.backend.offline.query(data) opts.to = 'zh'
else
if result.offline then return true end opts.from = 'zh'
opts.to = 'en'
local update = data.frontend:wait()
for _, backend in ipairs(data.backends) do
do_query(data, backend)
local name = backend.name
---@cast backend TransBackend
while result[name] == nil do
if not update(backend) then break end
end
if result[name] then return true end
end end
end
assert(opts.from and opts.to, 'opts.from and opts.to must be set at the same time')
return false opts.is_word = opts.is_word or Trans.util.is_word(str)
end,
--- TODO :More Strategys
}
-- HACK : Core process logic
local function process(opts)
opts = init_opts(opts)
local str = opts.str
if not str or str == '' then return end
-- Find in cache -- Find in cache
if Trans.cache[str] then if Trans.cache[str] then
local data = Trans.cache[str] local data = Trans.cache[str]
data.frontend:process(data) return data.frontend:process(data)
return
end end
-- Create new data
local data = Trans.data.new(opts) local data = Trans.data.new(opts)
if strategy[data.frontend.opts.query](data) then if Trans.strategy[data.frontend.opts.query](data) then
Trans.cache[data.str] = data Trans.cache[data.str] = data
data.frontend:process(data) data.frontend:process(data)
else else
@ -97,8 +48,17 @@ local function process(opts)
end end
---@class TransDataOption
---@field mode string?
---@field frontend string?
---@field from string? @Source language type
---@field to string? @Target language type
---@field is_word? boolean @Is the str a word
--- NOTE : Use coroutine to stop and resume the process (for animation)
---@class Trans ---@class Trans
---@field translate fun(opts: { frontend: string?, mode: string?}?) Translate string core function ---@field translate fun(opts: TransDataOption?) Translate string core function
return function(opts) return function(...) coroutine.wrap(process)(...) end
coroutine.wrap(process)(opts)
end

View File

@ -3,7 +3,6 @@ local fn, api = vim.fn, vim.api
---@class TransUtil ---@class TransUtil
local M = require 'Trans'.metatable 'util' local M = require 'Trans'.metatable 'util'
---Get selected text ---Get selected text
---@return string ---@return string
function M.get_select() function M.get_select()
@ -32,7 +31,27 @@ function M.get_select()
local e = #lines local e = #lines
lines[1] = lines[1]:sub(s_col) lines[1] = lines[1]:sub(s_col)
lines[e] = line:sub(1, e_col) lines[e] = line:sub(1, e_col)
return table.concat(lines) return table.concat(lines, ' ')
end
end
---Get selected text
---@return string
function M.get_lines()
local _start = vim.fn.getpos 'v'
local _end = vim.fn.getpos '.'
if _start[2] > _end[2] then
_start, _end = _end, _start
end
local s_row, e_row = _start[2], _end[2]
if s_row == e_row then
return vim.fn.getline(s_row)
else
local lines = vim.fn.getline(s_row, e_row)
return table.concat(lines, ' ')
end end
end end
@ -52,7 +71,8 @@ function M.get_str(mode)
return fn.input '需要翻译的字符串: ' return fn.input '需要翻译的字符串: '
end, end,
V = function() V = function()
print 'TODO' api.nvim_input '<Esc>'
return M.get_lines()
end, end,
})[mode]():match '^%s*(.-)%s*$' })[mode]():match '^%s*(.-)%s*$'
end end
@ -60,6 +80,7 @@ end
---Puase coroutine for {ms} milliseconds ---Puase coroutine for {ms} milliseconds
---@param ms integer ---@param ms integer
function M.pause(ms) function M.pause(ms)
assert(ms)
local co = coroutine.running() local co = coroutine.running()
vim.defer_fn(function() vim.defer_fn(function()
coroutine.resume(co) coroutine.resume(co)
@ -70,7 +91,7 @@ end
---Detect whether the string is English ---Detect whether the string is English
---@param str string ---@param str string
---@return boolean ---@return boolean
function M.is_English(str) function M.is_english(str)
local char = { str:byte(1, -1) } local char = { str:byte(1, -1) }
for i = 1, #str do for i = 1, #str do
if char[i] > 128 then if char[i] > 128 then
@ -114,7 +135,6 @@ function M.center(node, win_width)
end end
local str = node[1] local str = node[1]
win_width = str:width()
local space = math.max(0, math.floor((win_width - str:width()) / 2)) local space = math.max(0, math.floor((win_width - str:width()) / 2))
node[1] = string.rep(' ', space) .. str node[1] = string.rep(' ', space) .. str
return node return node
@ -174,7 +194,7 @@ end
---@param str string ---@param str string
---@return boolean ---@return boolean
function M.is_word(str) function M.is_word(str)
return str:match '%w+' == str return str:find '%W' == nil
end end
---@param list any[] ---@param list any[]
@ -204,6 +224,13 @@ function M.list_fields(list, field)
return ret return ret
end end
-- function M.checker(method, ...)
-- -- TODO :Use function programming to simplify the code
-- local params = { ... }
-- end
---@class Trans ---@class Trans
---@field util TransUtil ---@field util TransUtil
return M return M

View File

@ -124,7 +124,8 @@ function window:try_close()
} }
end end
api.nvim_win_close(self.winid, true)
pcall(api.nvim_win_close, self.winid, true)
end end
---Set window local highlight group ---Set window local highlight group
@ -161,21 +162,7 @@ window.__index = window
---@alias WindowOpts ---@alias WindowOpts
---|{style: string, border: string, focusable: boolean, noautocmd?: boolean, relative: string, width: integer, height: integer, col: integer, row: integer, zindex?: integer, title?: table | string} ---|{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 TransWindowOpts
local default_opts = {
enter = false,
winid = -1,
---@type WindowOpts
win_opts = {
style = 'minimal',
border = 'rounded',
focusable = false,
noautocmd = true,
},
}
---@class TransWindow ---@class TransWindow
@ -197,11 +184,34 @@ local default_opts = {
-- relative = relative, -- relative = relative,
-- } -- }
---@class TransWindowOpts
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,
},
animation = nil, ---@type table? Hover Window Animation
buffer = nil, ---@type TransBuffer attached buffer object
}
---Create new window ---Create new window
---@param opts TransWindowOpts window config ---@param opts TransWindowOpts window config
---@return TransWindow ---@return TransWindow
function window.new(opts) function window.new(opts)
opts = vim.tbl_deep_extend('keep', opts, default_opts) opts = vim.tbl_deep_extend('keep', opts, default_opts)
opts.animation = opts.animation or { interval = 12 }
local win = setmetatable(opts, window) local win = setmetatable(opts, window)
---@cast win TransWindow ---@cast win TransWindow

View File

@ -121,3 +121,34 @@ return M
-- iciba = 'iciba', -- iciba = 'iciba',
-- offline = '本地', -- offline = '本地',
-- } -- }
-- 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

@ -5,9 +5,11 @@ local strategy = {
pageup = function(hover) pageup = function(hover)
hover.buffer:normal 'gg' hover.buffer:normal 'gg'
end, end,
pagedown = function(hover) pagedown = function(hover)
hover.buffer:normal 'G' hover.buffer:normal 'G'
end, end,
pin = function(hover) pin = function(hover)
if hover.pin then if hover.pin then
return return
@ -27,9 +29,11 @@ local strategy = {
window:set('wrap', true) window:set('wrap', true)
end, end,
close = function(hover) close = function(hover)
hover:destroy() hover:destroy()
end, end,
toggle_entry = function(hover) toggle_entry = function(hover)
if api.nvim_get_current_win() ~= hover.window.winid then if api.nvim_get_current_win() ~= hover.window.winid then
api.nvim_set_current_win(hover.window.winid) api.nvim_set_current_win(hover.window.winid)
@ -48,6 +52,12 @@ local strategy = {
---@class TransHover ---@class TransHover
---@field execute fun(hover: TransHover, action: string) ---@field execute fun(hover: TransHover, action: string)
return function(hover, action) return function(hover, action)
-- TODO : return strategy[action](hover)
strategy[action](hover)
end end
-- This function will be called within coroutine, so we can't use __call
-- return setmetatable(strategy, {
-- __call = function(_, hover, action)
-- return strategy[action](hover)
-- end,
-- })

View File

@ -1,5 +1,6 @@
---@type Trans ---@type Trans
local Trans = require 'Trans' local Trans = require 'Trans'
local util = Trans.util
-- FIXME :Adjust Window Size -- FIXME :Adjust Window Size
@ -9,25 +10,93 @@ local Trans = require 'Trans'
---@field window TransWindow @hover window ---@field window TransWindow @hover window
---@field queue TransHover[] @hover queue for all hover instances ---@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 destroy_funcs fun(hover:TransHover)[] @functions to be executed when hover window is closed
---@field opts TransHoverOpts @options for hover window ---@field opts TransHoverOpts @hover window options
---@field pin boolean @whether hover window is pinned ---@field pin boolean @whether hover window is pinned
local M = Trans.metatable('frontend.hover', { local M = Trans.metatable('frontend.hover', {
ns = vim.api.nvim_create_namespace 'TransHoverWin', ns = vim.api.nvim_create_namespace 'TransHoverWin',
queue = {}, queue = {},
---@class TransHoverOpts: TransFrontendOpts
opts = {
---@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}} {{error_message}}',
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>;',
},
---@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 = '󰊿',
},
},
}) })
M.__index = M M.__index = M
--[[
Set up function which will be invoked when this module is loaded
Because the options are not loaded yet when this module is loaded
--]]
function M.setup() function M.setup()
local set = vim.keymap.set local set = vim.keymap.set
for action, key in pairs(M.opts.keymaps) do for action, key in pairs(M.opts.keymaps) do
set('n', key, function() set('n', key, function()
local instance = M.get_active_instance() local instance = M.get_active_instance()
if instance then if instance then
coroutine.wrap(instance.execute)(instance, action) coroutine.wrap(instance.execute)(instance, action)
else else
return key vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes(key, true, true, true), 'n', false)
end end
end) end)
end end
@ -88,17 +157,20 @@ end
function M:init_window(opts) function M:init_window(opts)
opts = opts or {} opts = opts or {}
local m_opts = self.opts local m_opts = self.opts
---@format disable-next
local option = { local option = {
buffer = self.buffer, buffer = self.buffer,
animation = m_opts.animation, animation = m_opts.animation,
win_opts = { win_opts = {
col = opts.col or 1,
row = opts.row or 1,
width = opts.width or m_opts.width,
height = opts.height or m_opts.height,
relative = opts.relative or 'cursor', 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 = m_opts.title,
title_pos = m_opts.title and 'center' or nil, title_pos = m_opts.title and 'center',
zindex = 100, zindex = 100,
}, },
} }
@ -118,7 +190,6 @@ end
---Get Check function for waiting ---Get Check function for waiting
---@return fun(backend: TransBackend): boolean ---@return fun(backend: TransBackend): boolean
function M:wait() function M:wait()
local util = Trans.util
local opts = self.opts local opts = self.opts
local buffer = self.buffer local buffer = self.buffer
local pause = util.pause local pause = util.pause
@ -126,7 +197,7 @@ function M:wait()
local spinner = Trans.style.spinner[opts.spinner] local spinner = Trans.style.spinner[opts.spinner]
local times = opts.width - spinner[1]:width() local times = opts.width - spinner[1]:width()
local size = #spinner local size = #spinner
local interval = math.floor(opts.timeout / opts.width) local interval = math.floor(opts.timeout / times)
self:init_window { self:init_window {
height = 2, height = 2,
@ -139,7 +210,7 @@ function M:wait()
local it = util.node.item local it = util.node.item
return function(backend) return function(backend)
cur = cur + 1 cur = cur + 1
buffer[1] = pr(backend.name_zh) buffer[1] = pr(backend.display_text)
buffer[2] = it { spinner[cur % size + 1] .. (cell):rep(cur), 'TransWaitting' } buffer[2] = it { spinner[cur % size + 1] .. (cell):rep(cur), 'TransWaitting' }
pause(interval) pause(interval)
return cur < times return cur < times
@ -154,7 +225,7 @@ function M:fallback()
local buffer = self.buffer local buffer = self.buffer
buffer:wipe() buffer:wipe()
buffer[1] = Trans.util.center(fallback_msg, opts.width) buffer[1] = util.center(fallback_msg, opts.width)
buffer:add_highlight(1, 'TransFailed') buffer:add_highlight(1, 'TransFailed')
if not self.window then if not self.window then
self:init_window { self:init_window {
@ -168,7 +239,7 @@ end
---Defer function when process done ---Defer function when process done
function M:defer() function M:defer()
Trans.util.main_loop(function() util.main_loop(function()
self.window:set('wrap', true) self.window:set('wrap', true)
self.buffer:set('modifiable', false) self.buffer:set('modifiable', false)
@ -204,7 +275,6 @@ function M:process(data)
return return
end end
local util = Trans.util
local opts = self.opts local opts = self.opts
local buffer = self.buffer local buffer = self.buffer
@ -227,11 +297,11 @@ function M:process(data)
local width = local width =
valid and valid and
(opts.auto_resize and (opts.auto_resize and
math.max( math.max(
math.min(opts.width, util.display_width(lines) + opts.padding), math.min(opts.width, util.display_width(lines) + opts.padding),
math.min(data.str:width(), opts.split_width) math.min(data.str:width(), opts.split_width)
) )
or opts.width) or opts.width)
or math.min(opts.width, util.display_width(lines) + opts.padding) or math.min(opts.width, util.display_width(lines) + opts.padding)
local height = math.min(opts.height, util.display_height(lines, width)) local height = math.min(opts.height, util.display_height(lines, width))

View File

@ -1,15 +1,15 @@
local Trans = require 'Trans' local Trans = require 'Trans'
local health, fn = vim.health, vim.fn local health, fn = vim.health, vim.fn
local ok = health.report_ok local ok = health.ok or health.report_ok
local warn = health.report_warn local warn = health.warn or health.report_warn
local error = health.report_error local error = health.error or health.report_error
local has = fn.has local has = fn.has
local executable = fn.executable local executable = fn.executable
local function check_neovim_version() local function check_neovim_version()
if has 'nvim-0.9' == 1 then if has 'nvim-0.9' == 1 then
ok [[You have [neovim-nightly] ]] ok [[You have [neovim-0.9] ]]
else else
warn [[Trans Title requires Neovim 0.9 or newer warn [[Trans Title requires Neovim 0.9 or newer
See neovim-nightly: [https://github.com/neovim/neovim/releases/tag/nightly] See neovim-nightly: [https://github.com/neovim/neovim/releases/tag/nightly]
@ -38,13 +38,13 @@ local function check_binary_dependencies()
'sqlite3', 'sqlite3',
} }
if has 'linux' == 1 then binary_dependencies[3] = ({
binary_dependencies[3] = 'festival' win = 'node',
elseif has 'mac' == 1 then mac = 'say',
binary_dependencies[3] = 'say' linux = 'festival',
else termux = 'termux-tts-speak',
binary_dependencies[3] = 'node' })[Trans.system]
end
for _, dep in ipairs(binary_dependencies) do for _, dep in ipairs(binary_dependencies) do
if executable(dep) == 1 then if executable(dep) == 1 then

View File

@ -5,41 +5,48 @@
local function metatable(folder_name, origin) local function metatable(folder_name, origin)
return setmetatable(origin or {}, { return setmetatable(origin or {}, {
__index = function(tbl, key) __index = function(tbl, key)
local status, result = pcall(require, ('Trans.%s.%s'):format(folder_name, key)) local found, result = pcall(require, ('Trans.%s.%s'):format(folder_name, key))
if status then if found then
tbl[key] = result rawset(tbl, key, result)
return result return result
end end
end, end,
}) })
end end
---@class string ---@class string
---@field width function @Get string display width ---@field width function @Get string display width
---@field play function @Use tts to play string ---@field play function @Use tts to play string
local sep = vim.loop.os_uname().sysname == 'Windows' and '\\' or '/' 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 separator = system == 'win' and '\\\\' or '/'
---@class Trans ---@class Trans
---@field style table @Style module ---@field style table @Style module
---@field cache table<string, TransData> @Cache for translated data object ---@field cache table<string, TransData> @Cache for translated data object
---@field plugin_dir string @Plugin directory ---@field plugin_dir string @Plugin directory
---@field separator string @Path separator ---@field separator string @Path separator
---@field system 'mac'|'win'|'termux'|'linux' @Path separator
---@field strategy table<string, fun(data: TransData):boolean> Translate string core function
local M = metatable('core', { local M = metatable('core', {
cache = {}, cache = {},
style = metatable 'style', style = metatable 'style',
plugin_dir = debug.getinfo(1, 'S').source:sub(2):match('(.-)lua' .. sep .. 'Trans'), strategy = metatable 'strategy',
separator = sep, separator = separator,
system = system,
plugin_dir = debug.getinfo(1, 'S').source:sub(2):match('(.-)lua' .. separator .. 'Trans'),
}) })
M.metatable = metatable M.metatable = metatable
---Get abs_path of file
---@param path string[]
---@param is_dir boolean?
---@return string
function M.relative_path(path, is_dir)
return M.plugin_dir .. table.concat(path, sep) .. (is_dir and sep or '')
end
return M return M

View File

@ -0,0 +1,27 @@
---Fallback query strategy
---@param data TransData
---@return boolean @true if query success
return function(data)
local result = data.result
local update
for _, backend in ipairs(data.backends) do
local name = backend.name
if backend.no_wait then
---@cast backend TransOfflineBackend
backend.query(data)
else
---@cast backend TransOnlineBackend
require 'Trans'.backend.do_query(data, backend)
update = update or data.frontend:wait()
while result[name] == nil and update(backend) do
end
end
---@cast backend TransBackend
if result[name] then return true end
end
return false
end

View File

@ -29,248 +29,461 @@
-- SOFTWARE. -- SOFTWARE.
return { return {
dots = { dots = {
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
}, },
dots_negative = { -- dots2 dots_negative = { -- dots2
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
}, },
dots_snake = { -- dots3 dots_snake = { -- dots3
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
}, },
dots_footsteps = { -- dots10 dots_footsteps = { -- dots10
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
}, },
dots_hop = { -- dots11 dots_hop = { -- dots11
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
}, },
line = { line = {
"-", '-',
"\\", '\\',
"|", '|',
"/", '/',
}, },
pipe = { pipe = {
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
}, },
dots_ellipsis = { -- simpleDots dots_ellipsis = { -- simpleDots
". ", '. ',
".. ", '.. ',
"...", '...',
" ", ' ',
}, },
dots_scrolling = { -- simpleDotsScrolling dots_scrolling = { -- simpleDotsScrolling
". ", '. ',
".. ", '.. ',
"...", '...',
" ..", ' ..',
" .", ' .',
" ", ' ',
}, },
star = { star = {
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
}, },
flip = { flip = {
"_", '_',
"_", '_',
"_", '_',
"-", '-',
"`", '`',
"`", '`',
"'", "'",
"´", '´',
"-", '-',
"_", '_',
"_", '_',
"_", '_',
}, },
hamburger = { hamburger = {
"", '',
"", '',
"", '',
}, },
grow_vertical = { -- growVertical grow_vertical = { -- growVertical
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
}, },
grow_horizontal = { -- growHorizontal grow_horizontal = { -- growHorizontal
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
}, },
noise = { noise = {
"", '',
"", '',
"", '',
}, },
dots_bounce = { -- bounce dots_bounce = { -- bounce
"", '',
"", '',
"", '',
"", '',
}, },
triangle = { triangle = {
"", '',
"", '',
"", '',
"", '',
}, },
arc = { arc = {
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
}, },
circle = { circle = {
"", '',
"", '',
"", '',
}, },
square_corners = { -- squareCorners square_corners = { -- squareCorners
"", '',
"", '',
"", '',
"", '',
}, },
circle_quarters = { -- circleQuarters circle_quarters = { -- circleQuarters
"", '',
"", '',
"", '',
"", '',
}, },
circle_halves = { -- circleHalves circle_halves = { -- circleHalves
"", '',
"", '',
"", '',
"", '',
}, },
dots_toggle = { -- toggle dots_toggle = { -- toggle
"", '',
"", '',
}, },
box_toggle = { -- toggle2 box_toggle = { -- toggle2
"", '',
"", '',
}, },
arrow = { arrow = {
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
"", '',
}, },
clock = { clock = {
"🕛 ", '🕛 ',
"🕐 ", '🕐 ',
"🕑 ", '🕑 ',
"🕒 ", '🕒 ',
"🕓 ", '🕓 ',
"🕔 ", '🕔 ',
"🕕 ", '🕕 ',
"🕖 ", '🕖 ',
"🕗 ", '🕗 ',
"🕘 ", '🕘 ',
"🕙 ", '🕙 ',
"🕚 ", '🕚 ',
}, },
earth = { earth = {
"🌍 ", '🌍 ',
"🌎 ", '🌎 ',
"🌏 ", '🌏 ',
}, },
moon = { moon = {
"🌑 ", '🌑 ',
"🌒 ", '🌒 ',
"🌓 ", '🌓 ',
"🌔 ", '🌔 ',
"🌕 ", '🌕 ',
"🌖 ", '🌖 ',
"🌗 ", '🌗 ',
"🌘 ", '🌘 ',
}, },
dots_pulse = { -- point dots_pulse = { -- point
"∙∙∙", '∙∙∙',
"●∙∙", '●∙∙',
"∙●∙", '∙●∙',
"∙∙●", '∙∙●',
"∙∙∙", '∙∙∙',
},
fistBump = {
'🤜    🤛 ',
'🤜    🤛 ',
'🤜    🤛 ',
' 🤜  🤛  ',
'  🤜🤛   ',
' 🤜✨🤛   ',
'🤜 ✨ 🤛  ',
},
monkey = {
'🙈 ',
'🙈 ',
'🙉 ',
'🙊 '
},
soccerHeader = {
' 🧑⚽️ 🧑 ',
'🧑 ⚽️ 🧑 ',
'🧑 ⚽️ 🧑 ',
'🧑 ⚽️ 🧑 ',
'🧑 ⚽️ 🧑 ',
'🧑 ⚽️ 🧑 ',
'🧑 ⚽️🧑 ',
'🧑 ⚽️ 🧑 ',
'🧑 ⚽️ 🧑 ',
'🧑 ⚽️ 🧑 ',
'🧑 ⚽️ 🧑 ',
'🧑 ⚽️ 🧑 ',
},
weather = {
'☀️ ',
'☀️ ',
'☀️ ',
'🌤 ',
'⛅️ ',
'🌥 ',
'☁️ ',
'🌧 ',
'🌨 ',
'🌧 ',
'🌨 ',
'🌧 ',
'🌨 ',
'',
'🌨 ',
'🌧 ',
'🌨 ',
'☁️ ',
'🌥 ',
'⛅️ ',
'🌤 ',
'☀️ ',
'☀️ ',
},
speaker = {
'🔈 ',
'🔉 ',
'🔊 ',
'🔉 ',
},
smiley = {
'😄 ',
'😝 ',
},
toggle = {
'',
''
},
toggle10 = {
'',
'',
''
},
toggle11 = {
'',
''
},
toggle12 = {
'',
''
},
toggle13 = {
'=',
'*',
'-'
},
toggle2 = {
'',
''
},
toggle3 = {
'',
''
},
toggle4 = {
'',
'',
'',
''
},
toggle5 = {
'',
''
},
toggle6 = {
'',
''
},
toggle7 = {
'',
'⦿'
},
toggle8 = {
'',
''
},
toggle9 = {
'',
''
},
star2 = {
'+',
'x',
'*'
},
orangeBluePulse = {
'🔸 ',
'🔶 ',
'🟠 ',
'🟠 ',
'🔶 ',
'🔹 ',
'🔷 ',
'🔵 ',
'🔵 ',
'🔷 ',
},
orangePulse = {
'🔸 ',
'🔶 ',
'🟠 ',
'🟠 ',
'🔶 '
},
mindblown = {
'😐 ',
'😐 ',
'😮 ',
'😮 ',
'😦 ',
'😦 ',
'😧 ',
'😧 ',
'🤯 ',
'💥 ',
'',
'  ',
'  ',
'  ',
},
hearts = {
'💛 ',
'💙 ',
'💜 ',
'💚 ',
'❤️ '
},
fingerDance = {
'🤘 ',
'🤟 ',
'🖖 ',
'',
'🤚 ',
'👆 '
},
christmas = {
'🌲',
'🎄'
},
circleHalves = {
'',
'',
'',
''
},
bouncingBall = {
'( ● )',
'( ● )',
'( ● )',
'( ● )',
'( ●)',
'( ● )',
'( ● )',
'( ● )',
'( ● )',
'(● )',
},
bluePulse = {
'🔹 ',
'🔷 ',
'🔵 ',
'🔵 ',
'🔷 '
},
betaWave = {
'ρββββββ',
'βρβββββ',
'ββρββββ',
'βββρβββ',
'ββββρββ',
'βββββρβ',
'ββββββρ',
}, },
} }

View File

@ -4,62 +4,63 @@ local util = require 'Trans'.util
---@field [1] string text to be rendered ---@field [1] string text to be rendered
---@field render fun(self: TransNode, buffer: TransBuffer, line: number, col: number) render the node ---@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
---@class TransItem : TransNode ---Basic item node
local item_meta = { ---@param tuple {[1]: string, [2]: string?}
---@param self TransItem ---@return TransItem
---@param buffer TransBuffer return function(tuple)
---@param line integer return setmetatable(tuple, mt)
---@param col integer end
render = function(self, buffer, line, col) end)()
if self[2] then
buffer:add_highlight(line, self[2], col, col + #self[1])
end
end,
}
---@class TransText : TransNode local text = (function()
---@field step string ---@class TransText : TransNode
---@field nodes TransNode[] ---@field step string
local text_meta = { ---@field nodes TransNode[]
---@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 local mt = {
node:render(buffer, line, col) ---@param self TransText
col = col + #node[1] + len ---@param buffer TransBuffer
end ---@param line integer
end, ---@param col integer
} render = function(self, buffer, line, col)
local nodes = self.nodes
local step = self.step
local len = step and #step or 0
item_meta.__index = item_meta for _, node in ipairs(nodes) do
text_meta.__index = text_meta node:render(buffer, line, col)
col = col + #node[1] + len
end
end,
}
mt.__index = mt
---Basic item node ---@param nodes {[number]: TransNode, step: string?}
---@param tuple {[1]: string, [2]: string?} ---@return TransText
---@return TransItem return function(nodes)
local function item(tuple) return setmetatable({
return setmetatable(tuple, item_meta) [1] = table.concat(util.list_fields(nodes, 1), nodes.step),
end step = nodes.step,
nodes = nodes,
}, mt)
end
---@param nodes {[number]: TransNode, step: string?} end)()
---@return TransText
local function text(nodes)
return setmetatable({
[1] = table.concat(util.list_fields(nodes, 1), nodes.step),
step = nodes.step,
nodes = nodes,
}, text_meta)
end
---@param args {[number]: TransNode, width: integer, spin: string?} ---@param args {[number]: TransNode, width: integer, spin: string?}
@ -85,7 +86,6 @@ end
---@class TransUtil ---@class TransUtil
---@field node TransNodes ---@field node TransNodes
---@class TransNodes ---@class TransNodes
return { return {
item = item, item = item,

View File

@ -6,6 +6,7 @@ _G.api = vim.api
_G.fn = vim.fn _G.fn = vim.fn
_G.mock = require 'luassert.mock' _G.mock = require 'luassert.mock'
_G.stub = require 'luassert.stub' _G.stub = require 'luassert.stub'
string.width = api.nvim_strwidth
---@param func fun(buffer: TransBuffer) ---@param func fun(buffer: TransBuffer)
---@return fun() ---@return fun()

View File

@ -1,9 +1,92 @@
---@diagnostic disable: param-type-mismatch
require 'test.setup' require 'test.setup'
local util = Trans.util local util = Trans.util
describe('util.display_height', with_buffer(function(buffer) describe('util.display_height', function()
--- TODO :
it('can calculate the height of lines when window with wrap option', 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)
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)

100
lua/test/window_spec.lua Normal file
View File

@ -0,0 +1,100 @@
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 +1,4 @@
.PHONE: test .PHONE: test
test: test:
nvim --headless --noplugin -u script/minimal_init.vim -c "PlenaryBustedDirectory lua/test/ { minimal_init = './scripts/minimal_init.vim' }" -c 'qa!' nvim --headless --noplugin -u scripts/minimal_init.vim -c "PlenaryBustedDirectory lua/test/ { minimal_init = './scripts/minimal_init.vim' }" -c 'qa!'

View File

@ -1,34 +1,39 @@
local api, fn = vim.api, vim.fn local api, fn = vim.api, vim.fn
--- INFO :Define plugin command --- INFO :Define plugin command
local Trans = require("Trans") local Trans = require 'Trans'
local command = api.nvim_create_user_command local command = api.nvim_create_user_command
command("Translate", function() command('Translate', function() Trans.translate() end,
Trans.translate() { desc = ' Translate cursor word' })
end, { desc = " Translate cursor word" })
command("TranslateInput", function() command('TranslateInput', function() Trans.translate { mode = 'i' } end,
Trans.translate({ mode = 'i' }) { desc = ' Translate input word' })
end, { desc = " Translate input word" })
command("TransPlay", function() command('TransPlay', function()
local util = Trans.util local util = Trans.util
local str = util.get_str(vim.fn.mode()) local str = util.get_str(vim.fn.mode())
if str and str ~= "" and util.is_English(str) then if str and str ~= '' and util.is_english(str) then
str:play() str:play()
end end
end, { desc = " Auto play" }) end, { desc = ' Auto play' })
string.width = api.nvim_strwidth string.width = api.nvim_strwidth
local system = Trans.system
local f = local f =
fn.has('linux') == 1 and ([[echo %q | festival --tts]]) (vim.fn.has 'wsl' == 1 or system == 'win') and
or fn.has('mac') == 1 and ([[say %q]]) 'powershell.exe -Command "Add-Type -AssemblyName System.speech;(New-Object System.Speech.Synthesis.SpeechSynthesizer).Speak(\\\"%s\\\")"' or
or 'node' .. Trans.relative_path { 'tts', 'say.js' } .. ' %q' 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) string.play = function(self)
fn.jobstart(f:format(self)) ---@diagnostic disable-next-line: param-type-mismatch
local s = string.gsub(self, '\"', ' ')
fn.jobstart(f:format(s))
end end

View File

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

View File

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