/
spell.lua
233 lines (217 loc) · 7.67 KB
/
spell.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
-- для работы нужны:
-- 1. dll от проекта http://nhunspell.sourceforge.net/
-- отсюда http://sourceforge.net/projects/nhunspell/files/ берётся архив с последней версией и из него нужны либо Hunspellx86.dll, либо Hunspellx64.dll.
-- именно эти dll чисто нативные, никакого .net-а в них нет. положить их нужно куда-нибудь, откуда они загрузятся, например в папку с фаром.
-- 2. словари. по умолчанию макрос настроен на вот этот http://extensions.openoffice.org/en/project/russian-dictionary-%D1%91 русский словарь (нужны файлы ru_RU_yo.aff и ru_RU_yo.dic),
-- и вот этот http://extensions.openoffice.org/en/project/english-dictionaries-apache-openoffice английский словарь (нужны файлы en_US.aff и en_US.dic).
-- по умолчанию словари ожидаются в %FARPROFILE%\Dictionaries\.
local F=far.Flags
local config=
{
--ru
{
dictionary=[[%FARPROFILE%\Dictionaries\ru_RU_yo.dic]],
affix=[[%FARPROFILE%\Dictionaries\ru_RU_yo.aff]],
regexstr=[[/[а-яёА-ЯЁ]+/]],
color={Flags=bit64.bor(F.FCF_FG_4BIT,F.FCF_BG_4BIT),ForegroundColor=0xff00000f,BackgroundColor=0xff000004},
active=true
},
--en
{
dictionary=[[%FARPROFILE%\Dictionaries\en_US.dic]],
affix=[[%FARPROFILE%\Dictionaries\en_US.aff]],
regexstr=[[/[a-zA-Z]+/]],
color={Flags=bit64.bor(F.FCF_FG_4BIT,F.FCF_BG_4BIT),ForegroundColor=0xff00000f,BackgroundColor=0xff000002},
active=true
}
}
local editors={}
local colorguid=win.Uuid("46CCE102-965A-4e2d-8263-B59F268C74C8")
local active=false
local ffi=require("ffi")
local C=ffi.C
ffi.cdef[[
void* HunspellInit(void* affixBuffer,size_t affixBufferSize,void* dictionaryBuffer,size_t dictionaryBufferSize,wchar_t* key);
void HunspellFree(void* handle);
bool HunspellSpell(void* handle,const wchar_t* word);
wchar_t** HunspellSuggest(void* handle,const wchar_t* word);
int lstrlenW(wchar_t* lpString);
]]
local hunspell=ffi.load("hunspell"..(win.GetEnv("PROCESSOR_ARCHITECTURE"):gsub("AMD64","x64")))
local function LoadFile(filename)
local file=io.open(filename,"rb")
local data
if file then
data=file:read("*a")
file:close()
local result=ffi.new("uint8_t[?]",#data)
ffi.copy(result,data,#data)
return result,#data
end
end
local function ExpandEnv(str) return (str:gsub("%%(.-)%%",win.GetEnv)) end
local function Init()
for _,v in ipairs(config) do
v.dict_data,v.dict_len=LoadFile(ExpandEnv(v.dictionary))
v.affix_data,v.affix_len=LoadFile(ExpandEnv(v.affix))
if v.dict_data and v.affix_data then
v.handle=hunspell.HunspellInit(v.affix_data,v.affix_len,v.dict_data,v.dict_len,nil)
v.regex=regex.new(v.regexstr)
else
v.active=false
end
end
Init=function() end
end
local function GetData(id)
local data=editors[id]
if not data then
editors[id]={start=0,finish=-1}
data=editors[id]
end
return data
end
local function ToWChar(str)
str=win.Utf8ToUtf16(str)
local result=ffi.new("wchar_t[?]",#str/2+1)
ffi.copy(result,str)
return result
end
local function ShowMenu(strings,wordLen)
local info=editor.GetInfo()
local menuShadowWidth=2
local menuOverheadWidth=menuShadowWidth+4 --6 => 2 рамка, 2 тень, 2 место для чекмарка
local menuOverheadHeight=3 --3 => 2 рамка, 1 тень
local menuWidth=0
local listItems={}
for _,v in ipairs(strings) do
menuWidth=math.max(menuWidth,v:len())
table.insert(listItems,{Flags=0,Text=v})
end
local menuHeight=1
local coorX=info.CurTabPos-info.LeftPos
local coorY=info.CurLine-info.TopScreenLine
local menuX=math.max(0,coorX-menuWidth-menuOverheadWidth+menuShadowWidth)
menuX=(info.WindowSizeX-coorX)>(coorX+2-wordLen) and (coorX+1) or menuX --меню справа или слева от слова?
local menuY=0
if (info.WindowSizeY-coorY-1)>coorY+1 then --меню сверху или снизу?
--снизу
menuY=coorY+2
menuHeight=info.WindowSizeY-menuY+1-menuOverheadHeight
menuHeight=math.min(menuHeight,#strings)
else
--сверху
menuY=coorY-#strings-1
if menuY<1 then menuY=1 end
menuHeight=coorY-menuY-1
end
--fix menu width
if (menuX+menuWidth+menuOverheadWidth)>info.WindowSizeX then
menuWidth=info.WindowSizeX-menuX-menuOverheadWidth
end
local items={
{"DI_LISTBOX",0,0,menuWidth+3,menuHeight+1,listItems,0,0,0,""}
}
local function DlgProc(dlg,msg,param1,param2)
end
local dialog=far.DialogInit(win.Uuid("ECD10910-8CC6-4685-AA8D-7D7413DD7D06"),menuX,menuY,menuX+menuWidth+3,menuY+menuHeight+1,nil,items,0,DlgProc)
local result=far.DialogRun(dialog)>0 and far.SendDlgMessage(dialog,F.DM_LISTGETCURPOS,1).SelectPos or nil
far.DialogFree(dialog)
return result
end
local function CheckSpell()
Init()
local pos,pos2=editor.GetInfo(-1).CurPos,0
local line=editor.GetString(-1,-1)
local linestr=line.StringText
local word=""
if pos<=linestr:len()+1 then
local slab=pos>1 and linestr:sub(1,pos-1):match('[%w_]+$') or ""
local tail=linestr:sub(pos):match('^[%w_]+') or ""
pos2=pos-slab:len()
word=slab..tail
end
if word~="" then
local word_data=ToWChar(word)
for _,v in ipairs(config) do
if v.active and v.regex:match(word) and not hunspell.HunspellSpell(v.handle,word_data) then
local suggestions=hunspell.HunspellSuggest(v.handle,word_data)
local ii,items=0,{}
while suggestions[ii]~=ffi.NULL do
table.insert(items,win.Utf16ToUtf8(ffi.string(suggestions[ii],2*C.lstrlenW(suggestions[ii]))))
ii=ii+1
end
if #items>0 then
local sel=ShowMenu(items,word:len())
if sel then
linestr=line.StringText:sub(1,pos2-1)..items[sel]..line.StringText:sub(pos2+word:len())
editor.SetString(-1,0,linestr,line.StringEol)
end
end
break
end
end
end
end
local function CheckSpellAll(ei)
Init()
local start,finish=ei.TopScreenLine,math.min(ei.TopScreenLine+ei.WindowSizeY-1,ei.TotalLines)
local regex=regex.new([[/\b\i+\b/]])
for ii=start,finish do
local line=editor.GetString(ei.EditorID,ii).StringText
local pos=1
while true do
local sbegin,send=regex:find(line,pos)
if not sbegin then break end
pos=send+1
local word=line:sub(sbegin,send)
for _,v in ipairs(config) do
if v.active and v.regex:match(word) and not hunspell.HunspellSpell(v.handle,ToWChar(word)) then
editor.AddColor(ei.EditorID,ii,sbegin,send,F.ECF_AUTODELETE,v.color,199,colorguid)
break
end
end
end
end
end
Event
{
group="EditorEvent";
action=function(id,event,param)
if event==F.EE_READ then
editors[id]={start=0,finish=-1}
end
if event==F.EE_CLOSE then
editors[id]=nil
end
if event==F.EE_REDRAW then
if active then
CheckSpellAll(editor.GetInfo(id))
end
end
end
}
Event
{
group="ExitFAR";
action=function()
for _,v in ipairs(config) do
if v.active and v.handle then
hunspell.HunspellFree(v.handle)
end
end
end
}
Macro
{
area="Editor";key="F3";description="check spell";
action=CheckSpell
}
Macro
{
area="Editor";key="ShiftF3";description="highlight spell errors";
action=function()
active=not active
editor.Redraw(-1)
end
}