-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathaskai.lua
402 lines (384 loc) · 13.1 KB
/
askai.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
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
local Info = Info or package.loaded.regscript or function(...) return ... end --luacheck: ignore 113/Info
local nfo = Info { _filename or ...,
name = "Ask AI";
description = "bring ChatGPT to Far";
version = "1.6"; --https://semver.org/lang/ru/
author = "jd";
url = "https://forum.farmanager.com/viewtopic.php?t=13447";
id = "4618DD57-B187-441D-BFE2-B3C7CAD37B39";
minfarversion = {3,0,0,6279,0}; --actl: LuaMacro 810
--files = "*.lua.cfg;*.preset";--todo
options = {
key = "CtrlB",
--keyList = "CtrlB:Hold",
--keyOutput = "CtrlB:Double",
keyCopy = "CtrlShiftIns",
sharedParams = { apibase=1, max_tokens=1, temperature=1, top_p=1, top_k=1, role=1 },
--smallDlg = true,
stdEnvs = {
apikey ={OPENAI_API_KEY=1},
apibase={OPENAI_BASE_URL=1, OPENAI_API_BASE=1},
model ={OPENAI_API_MODEL=1, OPENAI_MODEL=1},
},
State = {
isDlgOpened=false,
useSession=2,
useStream=1,
useWrap=1, -- wrap lines
wrapAt=80,
},
-- skip autodetection and use specified json module
-- the module must provide `encode`, `decode`, and (optionally) `null`.
--json_module="dkjson", -- http://dkolf.de/dkjson-lua/
};
--disabled = false;
}
if not nfo or nfo.disabled then return end
local O = nfo.options
local F = far.Flags
local cfgpath = (_filename or ...):match"^(.*)[\\/]"
local _tmp = far.InMyConfig and far.InMyConfig() --luacheck: globals far.InMyConfig -- far2m
or win.GetEnv("FARLOCALPROFILE")
local outputFilename --fwd decl.
local State do
local sh = sh or pcall(require,"sh") and require"sh" --LuaShell
local _shared = sh and sh._shared or {}
State = _shared.AskAI
if not State then
State = O.State
_shared.AskAI = State
end
end
local idProgress = win.Uuid"3E5021C5-47C7-4446-8E3B-13D3D9052FD8"
local function progress (text, title)
local MINLEN = 22
local len = math.max(text:len(), title and title:len() or 0, MINLEN)
local items = {
{F.DI_SINGLEBOX,0, 0,len+4,3,0,0,0,F.DIF_NONE, title},
{F.DI_TEXT, 0, 1,0, 1,0,0,0,F.DIF_CENTERGROUP, text},
}
return far.DialogInit(idProgress, -1, -1, len+4, 3, nil, items, F.FDLG_NONMODAL +(title and 0 or F.FDLG_KEEPCONSOLETITLE))
end
local function openOutput (mode)
local CP = 65001
local curModal = bit64.band(actl.GetWindowInfo().Flags, F.WIF_MODAL)==F.WIF_MODAL
local opened
for i=actl.GetWindowCount(),1,-1 do
local wi = actl.GetWindowInfo(i)
if wi.Type==F.WTYPE_EDITOR and wi.Name==outputFilename then
opened = true
if curModal then editor.Quit(wi.Id) end
break
end
end
if mode=="existing" then
if not (opened or win.GetFileAttr(outputFilename)) then
mf.beep(); return
end
elseif not opened then
win.DeleteFile(outputFilename)
end
local res
if not curModal then
local tryNotModal = F.EF_DISABLEHISTORY +F.EF_NONMODAL +F.EF_IMMEDIATERETURN +F.EF_OPENMODE_USEEXISTING
res = editor.Editor(outputFilename, nil, nil, nil, nil, nil, tryNotModal, nil, nil, CP)
end
if curModal or res==F.EEC_LOADING_INTERRUPTED then
editor.Editor(outputFilename, nil, nil, nil, nil, nil, F.EF_DISABLEHISTORY +F.EF_OPENMODE_NEWIFOPEN, nil, nil, CP)
end
end
local buf
local function _words (chunk)
--if chunk=="" then return function() end end
if chunk then
buf = buf..chunk
end
local eof = not chunk
return function()
if buf=="" then return nil end
local space, word, other = string.match(buf, "^(%s*)(%S+%-)(.*)") --hyphen
if not word then
space, word, other = string.match(buf, "^(%s*)(%S+)(%s.*)")
end
if word then
buf = other
return space, word
elseif eof then
space, word = string.match(buf, "(%s*)(.*)") -- will always match
buf = ""
return space, word
end
end
end
local menu, dialog, utils, default --fwd decl.
local function getCfg (cfgname)
if not cfgname then
local success, filename = pcall(utils.readFile, default)
local pathname = success and utils.pathjoin(cfgpath, filename)
local cfg = pathname and win.GetFileAttr(pathname) and utils.loadCfg(pathname)
return cfg and cfg.reachable and cfg
elseif type(cfgname)=="string" then
if cfgname=="" then return end -- use chooseCfg menu
local pathname
if cfgname:match"[\\/]" or cfgname:match"%.lua%.cfg$" then
pathname = far.ConvertPath(cfgname)
else
pathname = utils.pathjoin(cfgpath, cfgname..".lua.cfg")
end
local cfg = utils.loadCfg(pathname)
if not cfg.reachable then
error(("%s: dependencies not found\nSee help and %s"):format(cfg.info.name, cfg.info.url))
end
return cfg
end
end
local setBigCursor; if jit.os=="Windows" then
local ffi = require"ffi"
pcall(ffi.cdef, [[
//https://learn.microsoft.com/en-us/windows/console/console-cursor-info-str
typedef struct _CONSOLE_CURSOR_INFO {
DWORD dwSize;
BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
//https://learn.microsoft.com/en-us/windows/console/setconsolecursorinfo
BOOL SetConsoleCursorInfo(
HANDLE hConsoleOutput,
const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);
]])
local C = ffi.C
local hConsoleOutput = C.GetStdHandle(-11)
local ConsoleCursorInfo = ffi.new("CONSOLE_CURSOR_INFO",99,1)
function setBigCursor()
C.SetConsoleCursorInfo(hConsoleOutput, ConsoleCursorInfo)
end
end
local function askAI (prompt, cfgname)
local cfg = getCfg(cfgname) or cfgname
if not cfg then return menu.chooseCfg(prompt) end
local processStream, prompt, linewrap, stream = dialog(cfg, prompt, Editor.SelValue)
if not processStream then return end
utils.synchro(function() -- workaround for https://bugs.farmanager.com/view.php?id=3044
local wi = actl.GetWindowInfo()
assert(wi.Type==F.WTYPE_EDITOR and wi.Name==outputFilename, "oops, editor has not been opened")
local Id = wi.Id
editor.UndoRedo(Id, F.EUR_BEGIN)
local ei = editor.GetInfo(Id)
if linewrap=="dynamic" then
linewrap = ei.WindowSizeX - 5
end
local s = editor.GetString(Id, ei.TotalLines)
editor.SetPosition(Id, ei.TotalLines, s.StringLength+1)
local i = ei.TotalLines
if s.StringLength>0 then
editor.InsertString(Id)
end
if i>1 then
if string.len(editor.GetString(Id, i-1, 3))>0 then
editor.InsertString(Id)
end
editor.InsertString(Id)
end
local autowrap = bit64.band(ei.Options, F.EOPT_AUTOINDENT)~=0
if autowrap then editor.SetParam(Id, F.ESPT_AUTOINDENT, 0) end
editor.InsertText(Id, "> "..prompt.."\n\n")
local modal = bit64.band(F.WIF_MODAL, wi.Flags)~=0
local hDlg = not modal and progress("Waiting for data..")
local isFar3 = F.ACTL_GETFARMANAGERVERSION
if isFar3 then
editor.Redraw(Id)
setBigCursor()
end
if modal then
far.Message("Waiting for data..", "", "", "")
end
editor.SetTitle(Id, "Fetching response...")
local start = Far.UpTime
local function clockwatch ()
return math.ceil((Far.UpTime-start)/100)/10
end
local code = false
buf = ""
local before1stToken,total,started,title
local _,err = pcall(processStream, function (chunk, _title)
if not started then
repeat until not win.ExtractKeyEx() -- clean kbd buffer
before1stToken = clockwatch()
editor.SetTitle(Id, ("Fetching response [%s s]"):format(before1stToken))
if hDlg then
if stream then
hDlg:send(F.DM_SETTEXT, 2, "Streaming data..")
end
if _title then
title = _title
hDlg:send(F.DM_SETTEXT, 1, title)
end
end
started = true
end
for space,word in _words(chunk) do
editor.InsertText(Id, space:gsub("\r\n","\n")
:gsub("\r$","")) -- partial
if linewrap and not code and editor.GetInfo(Id).CurPos + word:len() > linewrap then
editor.InsertString(Id)
end
editor.InsertText(Id, word)
total = clockwatch()
editor.SetTitle(Id, ("Fetching response [%s s] +%s s"):format(before1stToken, total-before1stToken))
if linewrap then
local backticks = space:match"\n" and word:match("^```")
or space=="" and editor.GetString(Id,nil,3):match"^%s*```%S*$"
if backticks then code = not code end
end
end
if isFar3 then
editor.Redraw(Id)
setBigCursor()
end
end)
editor.InsertString(Id)
if err and err~="interrupted" then
far.Message(err:gsub("\t", " "), "Error", nil, "wl")
end
if autowrap then editor.SetParam(Id, F.ESPT_AUTOINDENT, 1) end
editor.UndoRedo(Id, F.EUR_END)
editor.SaveFile(Id)
if hDlg then hDlg:send(F.DM_CLOSE) end
title = (title and "["..title.."] " or "").."AI assistant response: "
local status = total and total.." s" or "Error!"
editor.SetTitle(Id, title..status)
end)
openOutput()
end
utils = assert(loadfile(cfgpath..package.config:sub(1,1).."utils.lua.1")) {
State=State, O=O,
cfgpath=cfgpath, name=nfo.name, _tmp=_tmp,
}
default = utils.pathjoin(cfgpath, "default")
outputFilename = utils.pathjoin(_tmp, "Ask AI.md")
menu = assert(loadfile(utils.pathjoin(cfgpath, "menu.lua.1"))) {
State=State, utils=utils, askAI=askAI,
cfgpath=cfgpath, default=default, name=nfo.name,
}
dialog = assert(loadfile(utils.pathjoin(cfgpath, "dialog.lua.1"))) {
State=State, O=O, utils=utils, menu=menu, askAI=askAI,
name=nfo.name, outputFilename=outputFilename,
}
nfo.config = function () mf.acall(menu.chooseCfg) end;
nfo.help = function () far.ShowHelp(cfgpath, nil, F.FHELP_CUSTOMPATH) end;
nfo.execute = function () mf.acall(askAI) end;
if Macro then
Macro { description=nfo.name;
area="Common"; key=O.key;
id="AF167BA4-E362-449E-A3F7-FF65E716F075";
condition=function() return not State.isDlgOpened end;
action=function()
mf.acall(askAI)
end;
}
Macro { description=nfo.name..": reopen output";
area="Common"; key=O.keyOutput or O.key..":Double";
id="89C2EB3B-7D32-4BC8-B5D0-874C0B34367D";
condition=function() return not State.isDlgOpened end;
action=function()
openOutput("existing")
end;
}
Macro { description=nfo.name..": choose cfg";
area="Common"; key=O.keyList or O.key..":Hold";
id="FD155A5E-3415-4A9A-BD91-1D7BA91097F0";
condition=function() return not State.isDlgOpened end;
action=function()
mf.acall(menu.chooseCfg)
end;
}
local codeStart,codeEnd = "^%s*```%S+$", "^(%s*)```$"
Macro { description=nfo.name..": copy code / paragraphs";
area="Editor"; key=O.keyCopy;
filemask="Ask AI.md";
id="353A2271-B739-41CE-AD47-BFDD109CBB17";
action=function()
local sel
if Object.Selected then
sel = Editor.SelValue:gsub("(%S[ -])\r?\n", "%1") -- unwrap
else -- find code block
local ei = editor.GetInfo()
local id = ei.EditorID
local start,finish,indent
for i=ei.CurLine,1,-1 do
local line = editor.GetString(id,i,3)
if line:match(codeStart) then
start = i; break
elseif line:match(codeEnd) and i~=ei.CurLine then
return
end
end
if not start then return end
local from = ei.CurLine + (start==ei.CurLine and 1 or 0)
for i=from,ei.TotalLines do
local line = editor.GetString(id,i,3)
indent = line:match(codeEnd)
if indent then
finish = i; break
elseif line:match(codeStart) then -- error
return
end
end
if not finish then -- code block not found
return
end
editor.Select(id, F.BTYPE_STREAM, start+1, 1, 0, finish-start)
sel = Editor.SelValue
if indent:len()>0 then -- trim extra indent
local arr = {}
for line,eol in sel:gmatch"(.-)(\r?\n)" do
if line:len()~=0 then
if line:sub(1, indent:len())==indent then
line = line:sub(indent:len()+1, -1)
else -- something is wrong
arr = false; break
end
end
arr[#arr+1] = line..eol
end
if arr then sel = table.concat(arr) end
end
end
mf.beep()
far.CopyToClipboard(sel)
end;
}
Macro { description=nfo.name..": unwrap text";
area="Editor"; key="AltF2";
filemask="Ask AI.md";
id="F7E6330A-182A-41BE-8EDF-0EFC4C8168A8";
action=function()
local ei = editor.GetInfo()
local id = ei.EditorID
local n = 0
for i=1, ei.TotalLines do
local line = editor.GetString(id,i,0)
if line.StringText:match"%S[ -]$" then
editor.SetString(id, i, line.StringText, "")
n = n+1
end
end
if n>0 and editor.SaveFile(id, ei.FileName) then --reload
local title = editor.GetTitle(id)
editor.Quit(id)
local EFLAGS = {EF_NONMODAL=1, EF_IMMEDIATERETURN=1, EF_DISABLEHISTORY=1}
editor.Editor(ei.FileName, title, nil,nil,nil,nil,EFLAGS,nil,nil,65001)
else
mf.beep()
end
end;
}
return
end
if _cmdline=="" then
sh.acall(askAI)
elseif _cmdline then
askAI(_cmdline)
else
return askAI
end