1 Commits

Author SHA1 Message Date
36865cc17d chore: add warning message 2023-04-07 19:57:52 +08:00
36 changed files with 833 additions and 1560 deletions

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.

250
README.md
View File

@ -2,48 +2,51 @@
<!--toc:start-->
- [Trans.nvim](#transnvim)
- [Trans.nvim](#transnvim) - [注意: 当前分支目前没有发布, README.md 的描述并不准确, 遇到问题请切换到 `main`分支或者联系我](#注意-当前分支目前没有发布-readmemd-的描述并不准确-遇到问题请切换到-main分支或者联系我)
- [特点](#特点)
- [屏幕截图](#屏幕截图)
- [演示](#演示)
- [离线查询](#离线查询)
- [在线查询演示 (有道)](#在线查询演示-有道)
- [\*在线查询\*\* (有道)](#在线查询-有道)
- [主题](#主题)
- [安装](#安装)
- [配置](#配置)
- [快捷键绑定](#快捷键绑定)
- [高亮组](#高亮组)
- [声明](#声明)
- [感谢](#感谢)
- [贡献](#贡献)
- [从 v1 (main)分支迁移](#从-v1-main分支迁移)
- [待办 (画大饼)](#待办-画大饼)
- [项目情况](#项目情况)
<!--toc:end-->
> **插件默认词库的路径为插件目录**
例如: `lazy` 用户应该在 `$HOME/.local/share/nvim/lazy/Trans.nvim`
### 注意: 当前分支目前没有发布, README.md 的描述并不准确, 遇到问题请切换到 `main`分支或者联系我
## 特点
- `使用纯 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`
@ -58,13 +61,13 @@
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)
在线查询配置见: [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)
### 主题
@ -136,7 +139,6 @@ use {
```lua
{
"JuanZoran/Trans.nvim",
build = function () require'Trans'.install() end,
keys = {
-- 可以换成其他你想映射的键
{ 'mm', mode = { 'n', 'x' }, '<Cmd>Translate<CR>', desc = ' Translate' },
@ -155,32 +157,40 @@ use {
<font color="#FF9900">**注意事项**: </font>
- `install.sh`
- 使用了 `wget`下载词库, 安装请确保你的环境变量中存在 wget
- install.sh 下载后会自动将词库解压, 并移动到 `$HOME/.vim/dict`文件夹下
- 目前仅在 `Ubuntu22.04`的环境下测试通过
> 如果上述条件不符合, 请删掉 `run = 'install.sh'`部分, 考虑手动安装词库
> 如果上述条件满足, 仍出现问题, 欢迎在 issue 里向我反馈,我会及时尝试解决
- 下载词典的过程中, 需要能够 `流畅的访问github下载`
如果下载出现问题, 正常是会自动下载
> 词库文件压缩包大小为: **281M**
> 解压缩后的大小大概为: 1.2G
> 词库文件压缩包大小为: **281M**
> 解压缩后的大小大概为: **1.2G**
- 安装后如果不能正常运行, 请尝试检查一下问题:
- 安装后如果不能正常运行, 清尝试运行 `checkhealth Trans`
- 本机是否已经安装了 `sqlite3`
> Linux 下安装:
> `sudo pacman -S sqlite # Arch`
> `sudo apt-get install sqlite3 libsqlite3-dev # Ubuntu`
- **`auto_play`** 的使用:
> **尝试运行 `checkhealth Trans`**
- `Linux` 需要安装`festival`
- **`auto_play`** 使用步骤:
> `sudo apt-get install festival festvox-kallpc16k`
> linux 只需要安装`festival`
> 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)
可以选择英音、美音、男声、女声
> 其他操作系统
- `Termux` 需要安装`termux-api`
- `Mac` 使用系统的`say`命令
- `Windows` 使用 `nodejs`的 say 模块, 如果你有更好的方案欢迎提供 PR
- 需要确保安装了`nodejs`
- 进入插件的`tts`目录运行`npm install`
> 如果`install`运行正常则自动安装,如果安装失败,请尝试手动安装
- 需要确保安装`nodejs`
- 进入插件的`tts`目录运行`npm install`
> 如果`install`运行正常则自动安装,如果安装失败,请尝试手动安装
- `title`的配置,只对`neovim 0.9+`版本有效
@ -234,16 +244,12 @@ use {
## 配置
详细见**wiki**: [基本配置说明](https://github.com/JuanZoran/Trans.nvim/wiki/%E9%85%8D%E7%BD%AE)
<details>
<summary>默认配置</summary>
详细见**wiki**: [配置说明](https://github.com/JuanZoran/Trans.nvim/wiki/%E9%85%8D%E7%BD%AE)
```lua
default_conf = {
require 'Trans'.setup {
---@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 = {
@ -288,13 +294,11 @@ default_conf = {
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>;',
pageup = '[[',
pagedown = ']]',
pin = '<leader>[',
close = '<leader>]',
toggle_entry = '<leader>;',
-- play = '_', -- Deprecated
},
---@type string[] auto close events
@ -345,8 +349,6 @@ default_conf = {
}
```
</details>
## 快捷键绑定
**示例:**
@ -359,90 +361,66 @@ vim.keymap.set({'n', 'x'}, 'mm', '<Cmd>Translate<CR>')
vim.keymap.set({'n', 'x'}, 'mk', '<Cmd>TransPlay<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'
}
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',
}
{
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',
}
}
```
</details>
## 声明
- 本插件词典基于[ECDICT](https://github.com/skywind3000/ECDICT)
@ -458,10 +436,6 @@ TransWeb = {
> 更新比较频繁, 文档先鸽了
> 如果你想要参加这个项目, 可以提 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] 快捷键定义

View File

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

View File

@ -1,13 +1,18 @@
---@class Baidu: TransBackendOnline
---@field conf { app_id: string, app_passwd: string }
---@class Baidu: TransOnlineBackend
---@field uri string api uri
---@field salt string
---@field app_id string
---@field app_passwd string
---@field disable boolean
local M = {
name = 'baidu',
display_text = '百度',
uri = 'https://fanyi-api.baidu.com/api/trans/vip/translate',
salt = tostring(math.random(bit.lshift(1, 15))),
method = 'get',
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
@ -22,17 +27,14 @@ local Trans = require 'Trans'
---@param data TransData
---@return BaiduQuery
function M.get_query(data)
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 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_conf.app_id,
appid = M.app_id,
salt = M.salt,
sign = sign,
}
@ -55,31 +57,14 @@ function M.formatter(body, data)
}
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
-- 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,

View File

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

View File

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

View File

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

View File

@ -1,68 +1,27 @@
local Trans = require 'Trans'
---@class TransBackend
---@field no_wait? boolean whether need to wait for the result
---@field name string
---@field display_text string?
---@field conf table? @User specific config
---@field all_name string[] @all backend name
---@field name string @backend name
---@field name_zh string @backend name in Chinese
---@class TransBackendOnline: TransBackend
---@class TransOnlineBackend: TransBackend
---@field uri string @request uri
---@field method 'get' | 'post' @request method
---@field formatter fun(body: table, data: TransData): TransResult|false|nil transform response body to TransResult
---@field get_query fun(data: TransData): table<string, any> @get query table
---@field error_message? fun(errorCode) @get error message
-- -@field header table<string, string>|fun(data: TransData): table<string, string> @request header
---@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
---@class TransBackendOffline: TransBackend
---@field query fun(data: TransData)
---@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 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'
@ -70,16 +29,33 @@ if file then
file:close()
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
---@field backend TransBackendCore
return setmetatable(M, {
---@field backend TransBackends
return setmetatable({
all_name = all_name,
}, {
__index = function(self, name)
---@type TransBackend
local backend = require('Trans.backend.' .. name)
backend.conf = user_conf[name]
self.sources[name] = backend
for key, value in pairs(user_conf[name] or {}) do
backend[key] = value
end
self[name] = backend
return backend
end,
})

View File

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

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

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

View File

@ -1,39 +1,53 @@
local Trans = require 'Trans'
---@class TransData: TransDataOption
---@field mode string @The mode of the str
---@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 is_word? boolean @Is the str a word
---@field trace table<string, string> debug message
---@field backends TransBackend[]
---@field backends table<string, TransBackend>
local M = {}
M.__index = M
---TransData constructor
---@param opts TransDataOption
---@param opts table
---@return TransData
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 data = setmetatable({
str = str,
mode = mode,
result = {},
trace = {},
}, M)
---@cast opts TransData
setmetatable(opts, 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
-- NOTE : whether should we use the default strategy
opts.frontend = Trans.frontend[strategy.frontend].new()
opts.backends = {}
data.is_word = Trans.util.is_word(str)
return opts
return data
end
---@class TransResult
@ -53,8 +67,12 @@ end
---@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

View File

@ -1,8 +0,0 @@
---@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,65 +1,32 @@
local Trans = require 'Trans'
---@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
local Trans = require 'Trans'
local conf = Trans.conf
local frontend_opts = conf.frontend
---@class TransFrontend
---@field opts? TransFrontendOpts options which user can set
---@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**
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
---@field frontend TransFrontend
return setmetatable(M, {
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 =
vim.tbl_extend('force', frontend.opts or {}, default_frontend, Trans.conf.frontend[name])
frontend.opts = opts
self[name] = frontend
if frontend.setup then
frontend.setup()
end
rawset(self, name, frontend)
return frontend
end,
})

View File

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

View File

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

View File

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

View File

@ -1,31 +1,44 @@
---@class Trans
local Trans = require 'Trans'
---@alias TransMode 'visual' 'input'
local default_strategy = {
frontend = 'hover',
backend = {
'offline',
-- 'youdao',
-- 'baidu',
},
}
local function set_strategy_opts(conf)
local all_backends = Trans.backend.all_name
local g_strategy = conf.strategy
Trans.conf = {
---@type string the directory for database file and password file
dir = require 'Trans'.plugin_dir,
---@type 'default' | 'dracula' | 'tokyonight' global Trans theme [@see lua/Trans/style/theme.lua]
theme = 'default',
---@type table<TransMode, { frontend:string, backend:string | string[] }> fallback strategy for mode
-- input = {
-- visual = {
-- ...
strategy = vim.defaulttable(function()
return setmetatable({}, default_strategy)
end),
frontend = {},
}
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
@ -35,12 +48,16 @@ return function(opts)
Trans.conf = vim.tbl_deep_extend('force', Trans.conf, opts)
end
local conf = Trans.conf
conf.dir = vim.fn.expand(conf.dir)
conf.dir = vim.fn.expand(conf.dir)
-- INFO : set highlight
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)
set_strategy_opts(conf)
define_highlights(conf)
if Trans.conf.warning then
vim.notify([[
新版本v2已经发布, 见:
https://github.com/JuanZoran/Trans.nvim
请使用当前默认(v2)分支
]], vim.log.levels.WARN)
end
end

View File

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

View File

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

View File

@ -124,8 +124,7 @@ function window:try_close()
}
end
pcall(api.nvim_win_close, self.winid, true)
api.nvim_win_close(self.winid, true)
end
---Set window local highlight group
@ -162,7 +161,21 @@ 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}
---|{style: string, border: string, focusable: boolean, noautocmd?: boolean, relative: string, width: integer, height: integer, col: integer, row: integer, zindex?: integer, title?: table | string}
---@class TransWindowOpts
local default_opts = {
enter = false,
winid = -1,
---@type WindowOpts
win_opts = {
style = 'minimal',
border = 'rounded',
focusable = false,
noautocmd = true,
},
}
---@class TransWindow
@ -184,34 +197,11 @@ window.__index = window
-- 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
---@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

View File

@ -121,34 +121,3 @@ return M
-- iciba = 'iciba',
-- 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,11 +5,9 @@ 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
@ -29,11 +27,9 @@ local strategy = {
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)
@ -52,12 +48,6 @@ local strategy = {
---@class TransHover
---@field execute fun(hover: TransHover, action: string)
return function(hover, action)
return strategy[action](hover)
-- TODO :
strategy[action](hover)
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,6 +1,5 @@
---@type Trans
local Trans = require 'Trans'
local util = Trans.util
-- FIXME :Adjust Window Size
@ -10,93 +9,25 @@ local util = Trans.util
---@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 @hover window options
---@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 = {},
---@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
--[[
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()
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)
return key
end
end)
end
@ -157,20 +88,17 @@ end
function M:init_window(opts)
opts = opts or {}
local m_opts = self.opts
---@format disable-next
local option = {
buffer = self.buffer,
animation = m_opts.animation,
win_opts = {
col = opts.col or 1,
row = opts.row or 1,
width = opts.width or m_opts.width,
height = opts.height or m_opts.height,
relative = opts.relative or 'cursor',
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',
title_pos = m_opts.title and 'center' or nil,
zindex = 100,
},
}
@ -190,6 +118,7 @@ end
---Get Check function for waiting
---@return fun(backend: TransBackend): boolean
function M:wait()
local util = Trans.util
local opts = self.opts
local buffer = self.buffer
local pause = util.pause
@ -197,7 +126,7 @@ function M:wait()
local spinner = Trans.style.spinner[opts.spinner]
local times = opts.width - spinner[1]:width()
local size = #spinner
local interval = math.floor(opts.timeout / times)
local interval = math.floor(opts.timeout / opts.width)
self:init_window {
height = 2,
@ -210,7 +139,7 @@ function M:wait()
local it = util.node.item
return function(backend)
cur = cur + 1
buffer[1] = pr(backend.display_text)
buffer[1] = pr(backend.name_zh)
buffer[2] = it { spinner[cur % size + 1] .. (cell):rep(cur), 'TransWaitting' }
pause(interval)
return cur < times
@ -225,7 +154,7 @@ function M:fallback()
local buffer = self.buffer
buffer:wipe()
buffer[1] = util.center(fallback_msg, opts.width)
buffer[1] = Trans.util.center(fallback_msg, opts.width)
buffer:add_highlight(1, 'TransFailed')
if not self.window then
self:init_window {
@ -239,7 +168,7 @@ end
---Defer function when process done
function M:defer()
util.main_loop(function()
Trans.util.main_loop(function()
self.window:set('wrap', true)
self.buffer:set('modifiable', false)
@ -275,6 +204,7 @@ function M:process(data)
return
end
local util = Trans.util
local opts = self.opts
local buffer = self.buffer
@ -297,11 +227,11 @@ function M:process(data)
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)
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))

View File

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

View File

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

View File

@ -1,27 +0,0 @@
---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,461 +29,248 @@
-- 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 = {
'',
''
},
star2 = {
'+',
'x',
'*'
},
orangeBluePulse = {
'🔸 ',
'🔶 ',
'🟠 ',
'🟠 ',
'🔶 ',
'🔹 ',
'🔷 ',
'🔵 ',
'🔵 ',
'🔷 ',
},
orangePulse = {
'🔸 ',
'🔶 ',
'🟠 ',
'🟠 ',
'🔶 '
},
mindblown = {
'😐 ',
'😐 ',
'😮 ',
'😮 ',
'😦 ',
'😦 ',
'😧 ',
'😧 ',
'🤯 ',
'💥 ',
'',
'  ',
'  ',
'  ',
},
hearts = {
'💛 ',
'💙 ',
'💜 ',
'💚 ',
'❤️ '
},
fingerDance = {
'🤘 ',
'🤟 ',
'🖖 ',
'',
'🤚 ',
'👆 '
},
christmas = {
'🌲',
'🎄'
},
circleHalves = {
'',
'',
'',
''
},
bouncingBall = {
'( ● )',
'( ● )',
'( ● )',
'( ● )',
'( ●)',
'( ● )',
'( ● )',
'( ● )',
'( ● )',
'(● )',
},
bluePulse = {
'🔹 ',
'🔷 ',
'🔵 ',
'🔵 ',
'🔷 '
},
betaWave = {
'ρββββββ',
'βρβββββ',
'ββρββββ',
'βββρβββ',
'ββββρββ',
'βββββρβ',
'ββββββρ',
"∙∙∙",
"●∙∙",
"∙●∙",
"∙∙●",
"∙∙∙",
},
}

View File

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

View File

@ -6,7 +6,6 @@ _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()

View File

@ -1,92 +1,9 @@
---@diagnostic disable: param-type-mismatch
require 'test.setup'
local util = Trans.util
describe('util.display_height', function()
describe('util.display_height', with_buffer(function(buffer)
--- TODO :
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)
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 +1,4 @@
.PHONE: test
test:
nvim --headless --noplugin -u scripts/minimal_init.vim -c "PlenaryBustedDirectory lua/test/ { minimal_init = './scripts/minimal_init.vim' }" -c 'qa!'
nvim --headless --noplugin -u script/minimal_init.vim -c "PlenaryBustedDirectory lua/test/ { minimal_init = './scripts/minimal_init.vim' }" -c 'qa!'

View File

@ -1,39 +1,34 @@
local api, fn = vim.api, vim.fn
--- INFO :Define plugin command
local Trans = require 'Trans'
local Trans = require("Trans")
local command = api.nvim_create_user_command
command('Translate', function() Trans.translate() end,
{ desc = ' Translate cursor word' })
command("Translate", function()
Trans.translate()
end, { desc = " Translate cursor word" })
command('TranslateInput', function() Trans.translate { mode = 'i' } end,
{ desc = ' Translate input word' })
command("TranslateInput", function()
Trans.translate({ mode = 'i' })
end, { desc = " Translate input word" })
command('TransPlay', function()
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
if str and str ~= "" and util.is_English(str) then
str:play()
end
end, { desc = ' Auto play' })
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'
fn.has('linux') == 1 and ([[echo %q | festival --tts]])
or fn.has('mac') == 1 and ([[say %q]])
or 'node' .. Trans.relative_path { 'tts', 'say.js' } .. ' %q'
string.play = function(self)
---@diagnostic disable-next-line: param-type-mismatch
local s = string.gsub(self, '\"', ' ')
fn.jobstart(f:format(s))
fn.jobstart(f:format(self))
end

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))