|
local Info = Info or package.loaded.regscript or function(...) return ... end --luacheck: ignore 113/Info |
|
local nfo = Info { _filename or ..., |
|
name = "bito.ai code assistant"; |
|
description = "bring ChatGPT to Far"; |
|
version = "0.10+popen"; --http://semver.org/lang/ru/ |
|
author = "jd"; |
|
url = "https://forum.farmanager.com/viewtopic.php?t=13283"; |
|
download = "https://github.com/gitbito/CLI/raw/main/version-3.6/bito.exe", |
|
www = "https://bito.ai/", |
|
webchat = "https://alpha.bito.ai/bitoai/home", |
|
prompthub = "https://prompthub.bito.ai/", |
|
id = "E8B4B673-212E-495D-A19D-FEFDB9DE8AF4"; |
|
minfarversion = {3,0,0,6279,0}; --actl: LuaMacro 810 |
|
|
|
--disabled = false; |
|
options = { |
|
linewrap = 80, |
|
}; |
|
} |
|
if not nfo or nfo.disabled then return end |
|
local O = nfo.options |
|
local F = far.Flags |
|
local idProgress = win.Uuid"3E5021C5-47C7-4446-8E3B-13D3D9052FD8" |
|
local function progress (text, title) |
|
local len = math.max(text:len(), title and title:len() or 0, 7) |
|
local items = { |
|
--[[01]] {F.DI_SINGLEBOX,0,0,len+4,3,0,0,0, 0, title}, |
|
--[[02]] {F.DI_TEXT, 2,1, 0,1,0,0,0,F.DIF_CENTERGROUP, text}, |
|
} |
|
return far.DialogInit(idProgress, -1, -1, len+4, 3, nil, items, F.FDLG_NONMODAL) |
|
end |
|
|
|
local function _words (getchunk) |
|
local buf, eof = "", nil |
|
return function() |
|
while true do |
|
if not eof then |
|
local chunk = getchunk() |
|
if chunk then |
|
buf = buf..chunk |
|
end |
|
eof = not chunk |
|
end |
|
if buf == "" then return end |
|
local space, word, other = buf:match("^(%s*)(%S+)(%s.*)") |
|
if space then |
|
buf = other |
|
return space, word |
|
elseif eof then |
|
space, word = buf:match("(%s*)(.*)") -- will always match |
|
buf = "" |
|
return space, word |
|
end |
|
end |
|
end |
|
end |
|
|
|
local function writeFile (pathname, content) |
|
local fp = assert(io.open(pathname, "w")) |
|
fp:write(content) |
|
fp:close() |
|
end |
|
|
|
local todel = {} |
|
local function cleanup () |
|
for _, pathname in ipairs(todel) do |
|
win.DeleteFile(pathname) |
|
end |
|
todel = {} |
|
end |
|
local root = win.GetEnv("FARLOCALPROFILE").."\\" |
|
local function tmpfile (filename, content) |
|
local pathname = root..filename |
|
writeFile(pathname, content) |
|
table.insert(todel, pathname) |
|
return pathname |
|
end |
|
|
|
local outputFile = "bito.md" |
|
local outputFullPath = root..outputFile |
|
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==outputFullPath then |
|
opened = true |
|
if curModal then editor.Quit(wi.Id) end |
|
break |
|
end |
|
end |
|
if mode=="existing" then |
|
if not (opened or win.GetFileAttr(outputFullPath)) then |
|
mf.beep(); return |
|
end |
|
elseif not opened then |
|
win.DeleteFile(outputFullPath) |
|
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(outputFullPath, nil, nil, nil, nil, nil, tryNotModal, nil, nil, CP) |
|
end |
|
if curModal or res==F.EEC_LOADING_INTERRUPTED then |
|
editor.Editor(outputFullPath, nil, nil, nil, nil, nil, F.EF_DISABLEHISTORY +F.EF_OPENMODE_NEWIFOPEN, nil, nil, CP) |
|
end |
|
end |
|
|
|
local function isOpened (filename) |
|
for i=1,actl.GetWindowCount() do |
|
if actl.GetWindowInfo(i).Name==filename then |
|
return true |
|
end |
|
end |
|
end |
|
|
|
local function check (key) |
|
repeat |
|
local k = win.ExtractKeyEx() |
|
if k and far.InputRecordToName(k)==key then return true end |
|
until not k |
|
end |
|
|
|
local _prompt = "Ask any technical question / use {{%code%}} as seltext placeholder" |
|
local idInput = win.Uuid"58DD9ECD-CFFA-472E-BFD7-042295C86CAE" |
|
local function bito (prompt) |
|
prompt = prompt or far.InputBox(idInput, nfo.name, _prompt, "bito.ai prompt", nil, nil, nil, F.FIB_NONE) |
|
if not prompt then return end |
|
local ctx = Editor.SelValue |
|
local cmd = ('"%s" -c "%s" -f "%s" -p "%s"'):format( |
|
"bito.exe", |
|
root.."ctx.bito", |
|
tmpfile("file.bito", ctx=="" and " " or ctx), |
|
tmpfile("prompt.bito", prompt) |
|
) |
|
if not isOpened(outputFullPath) then |
|
win.DeleteFile(root.."ctx.bito") |
|
end |
|
far.Timer(0, function (t) -- workaround for https://bugs.farmanager.com/view.php?id=3044 |
|
t:Close() |
|
local wi = actl.GetWindowInfo() |
|
assert(wi.Type==F.WTYPE_EDITOR, "oops, editor has not been opened") |
|
local Id = wi.Id |
|
editor.SetTitle(Id, "Fetching response...") |
|
local modal = bit64.band(F.WIF_MODAL, wi.Flags)~=0 |
|
local hDlg = not modal and progress("Waiting for data..") |
|
if modal then |
|
far.Message("Waiting for data..", "", "", "") |
|
end |
|
editor.UndoRedo(Id, F.EUR_BEGIN) |
|
local ei = editor.GetInfo(Id) |
|
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) |
|
editor.InsertString(Id) |
|
i = i+2 |
|
editor.SetPosition(Id, i) |
|
end |
|
editor.InsertText(Id, "> "..prompt.."\n\n") |
|
far.Text() |
|
|
|
local pipe = io.popen(('"%s" 2>&1'):format(cmd), "r") |
|
local function getchunkUtf8() |
|
if check"Esc" then pipe:close(); return end |
|
local chunk = pipe:read(5) |
|
if chunk then |
|
while not chunk:isvalid() do |
|
local extra = pipe:read(1) |
|
if extra then chunk = chunk..extra; else break; end |
|
end |
|
return chunk |
|
end |
|
pipe:close() |
|
end |
|
local start = Far.UpTime |
|
--[[ |
|
for line in pipe:lines() do |
|
editor.SetString(Id, i, line,"\n") |
|
editor.InsertString(Id) |
|
i = i+1 |
|
editor.Redraw(Id) |
|
end |
|
--]] |
|
local linewrap = O.linewrap or ei.WindowSizeX-5 |
|
local autowrap = bit64.band(ei.Options, F.EOPT_AUTOINDENT)~=0 |
|
if autowrap then editor.SetParam(Id, F.ESPT_AUTOINDENT, 0) end |
|
local code = false |
|
for space,word in _words(getchunkUtf8) do |
|
if start then |
|
if hDlg then |
|
hDlg:send(F.DM_SETTEXT, 2, "Streaming data..") |
|
hDlg:send(F.DM_SETTEXT, 1, (math.ceil((Far.UpTime-start)/100)/10).." s") |
|
end |
|
start = false |
|
cleanup() |
|
end |
|
editor.InsertText(Id, space) |
|
if not code and editor.GetInfo(Id).CurPos + word:len() > linewrap then |
|
editor.InsertText(Id, "\n") |
|
end |
|
editor.InsertText(Id, word) |
|
editor.Redraw(Id) |
|
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 |
|
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 |
|
editor.SetTitle(Id, "bito.ai response:") |
|
end) |
|
|
|
openOutput() |
|
end |
|
|
|
if Macro then |
|
Macro { description="Ask BitoAI"; |
|
area="Common"; key="CtrlB"; |
|
id="4AFE2367-4DAC-4A74-B1EE-9F14C42991CB"; |
|
action=function() |
|
mf.acall(bito) |
|
end; |
|
} |
|
Macro { description="Ask BitoAI: reopen output"; |
|
area="Common"; key="CtrlB:Double"; |
|
id="69EEB3AE-9EF3-4B51-B4A0-2585DFA30136"; |
|
action=function() |
|
openOutput("existing") |
|
end; |
|
} |
|
local codeStart,codeEnd = "^%s*```%S+$", "^%s*```$" |
|
Macro { description="Ask BitoAI: copy code / paragraphs"; |
|
area="Editor"; key="CtrlShiftIns"; |
|
filemask=outputFile; |
|
id="1EF69A5A-D048-42E9-BD35-7D1AE92329DE"; |
|
action=function() |
|
if not Object.Selected then |
|
local ei = editor.GetInfo() |
|
local id = ei.EditorID |
|
local start,finish |
|
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) |
|
if line:match(codeEnd) then |
|
finish = i; break |
|
elseif line:match(codeStart) then -- error |
|
return |
|
end |
|
end |
|
if not finish then |
|
return |
|
end |
|
editor.Select(id, F.BTYPE_STREAM, start+1, 1, 0, finish-start) |
|
end |
|
mf.beep() |
|
far.CopyToClipboard((Editor.SelValue:gsub(" \r?\n"," "))) |
|
end; |
|
} |
|
Macro { description="Ask BitoAI: unwrap text"; |
|
area="Editor"; key="AltF2"; |
|
filemask=outputFile; |
|
id="A70AD88D-A68D-43DF-8D28-5E69547C7925"; |
|
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(bito) |
|
elseif _cmdline then |
|
bito(_cmdline) |
|
else |
|
return bito |
|
end |