Last active
June 29, 2025 22:11
-
-
Save johnd0e/ee66b38aae026a8ecef4b36cd45c036a to your computer and use it in GitHub Desktop.
[FAR macro] Incremental search in editor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--https://forum.farmanager.com/viewtopic.php?p=180768#p180768 | |
-- Alternative to https://github.com/FarGroup/FarManager/pull/965 | |
F = far.Flags | |
local key | |
Macro | |
description: "fix: AltGr -> RAlt" | |
area: "Common" | |
key: "LCtrlRAlt" | |
condition: -> | |
win.Sleep 10 | |
if rec = win.ExtractKeyEx! | |
if rec.VirtualKeyCode==18 | |
key = (0==bit64.band rec.ControlKeyState, F.LEFT_ALT_PRESSED) and "RAlt" or "AltRAlt" | |
return 70 | |
id: "E5FABFD8-25A0-4018-8F52-22C0D575C5B7" | |
action: -> mf.eval key, 2 | |
Macro | |
description: "fix: Shift+AltGr -> RAltShift" | |
area: "Common" | |
key: "LCtrlRAltShift" | |
condition: -> | |
win.Sleep 10 | |
if rec = win.ExtractKeyEx! | |
if rec.VirtualKeyCode==18 | |
return 70 | |
id: "5FE64CB0-63C7-48A6-9ABB-5972145179EB" | |
action: -> mf.eval "RAltShift", 2 | |
-- test | |
NoMacro | |
area:"Common" | |
key:"RAlt RAltShift" | |
priority:100 | |
action: -> far.Show mf.akey 1,1 | |
NoMacro | |
area:"Common" | |
key:"/L(Alt|Ctrl)R\\2/" -- https://bugs.farmanager.com/view.php?id=4077 | |
priority:100 | |
action: -> far.Show mf.akey 1,1 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
" | |
.Language=Russian,Russian (Русский) | |
.PluginContents=IncSearch | |
.Options CtrlStartPosChar=^ | |
@Contents | |
$ #Инкрементальный поиск# | |
Инкрементальный поиск — это поиск в редакторе без использования диалогового окна. | |
Введённый текст ищется сразу по мере его набора, и найденное индицируется выделением. | |
Кроме того подсвечиваются все вхождения искомой строки, и подсветка остаётся после | |
завершения поиска. | |
Для активации инкрементального поиска по умолчанию служит правый #Alt# (настраивается в опциях, см. ниже). | |
• ^#RAlt# (одиночное нажатие/отпускание) | |
Если в текущей строке есть выделенный текст — он подхватывается. | |
Если ничего не введено, то можно вернуть строку из предыдущего поиска, нажав #BS#. | |
• ^#LCtrl#+#RAlt# / #RCtrl#+#RAlt# | |
- ^при выделенном тексте: ищет предыдущее / следующее его вхождение; | |
- ^если выделения нет - продолжает предыдущий поиск с текущего места назад / вперёд. | |
@=- | |
Активный режим инкрементального поиска индицируется в строке статуса. | |
• ^Нажатие какой-либо алфавитно-цифровой (или символьной) клавиши добавляет символ к искомой строке. | |
#AltRight# — расширяет выделение до конца слова. | |
• Нажатие любой функциональной клавиши (кроме перечисленных тут) отключает режим поиска, | |
клавиша передаётся на обработку фару. | |
(Это касается клавиш управления курсором, и всех клавиш с модификаторами #Ctrl#/#Alt#/#Shift#). | |
• Для прекращения поиска можно также воспользоваться повторным нажатием #RAlt#. | |
• #Esc# — прекращает поиск и восстанавливает исходную позицию. | |
• #BS# — удалить последний введённый символ. | |
При пустой строке: восстановить предыдущую строку. | |
• #CtrlBS# — очистить строку поиска. | |
• #ShiftIns#/#CtrlV# — вставить строку из буфера обмена. | |
• #RCtrl# | #ShiftF7# | #F3# | #Enter# — найти следующее вхождение строки. | |
• #LCtrl# | #AltF7# | #ShiftF3# | #ShiftEnter# — найти предыдущее вхождение строки. | |
• #CtrlA#: открыть диалог с результатами поиска всех вхождений. | |
Если при поиске строка не найдена в оставшейся части текста, то для того, | |
чтобы попытаться найти её во всём тексте, нужно продолжать удерживать клавишу ввода символа | |
или одну из комбинаций продолжения поиска. | |
@=- | |
При поиске доступны опции, состояние которых индицируется в строке статуса: | |
• #Alt#+Option | |
#R#: ^Reverse - поиск в обратном направлении. | |
#Q#: ExtraQuick - каждое нажатие не только прибавляет символ к строке поиска, но и ищет следующее вхождение. | |
#C#: CaseSensitive - чувствительность к регистру. | |
#W#: WholeWords - только целые слова. | |
@=- | |
В начале скрипта в переменной #options# можно задать ряд параметров: | |
• ^Клавиша активации: #RAlt# / #LAlt#. | |
• #keepHighlight#: сохранять ли подсветку при выходе из режима инкрементного поиска. | |
• #HighlightMinLen#: все вхождения искомой подстроки подсвечиваются если её длина достигает указанного значения. | |
Для более коротких строк подсвечиваются только целые слова, и при полном совпадении регистра. | |
• #CtrlA#: диалог с результатами поиска всех вхождений можно использовать штатный #far#, либо плагиновый - | |
#lfsearch# / #editfind#. | |
• #FoundMargin#: можно задать отступ от границы экрана при позиционирования найденного (если оно за пределами текущего экрана). | |
• #search#: для опций поиска можно задать начальные значения, имеет смысл для #ExtraQuick#. | |
• #useBM#: установка закладки перед началом поиска (при этом по #Esc# позиция не восстанавливается). | |
@-- | |
" | |
Info = Info or package.loaded.regscript or (...) -> ... | |
nfo = Info { _filename or ..., | |
name: "IncSearch" | |
description: "Incremental search in editor" | |
version: "3-alpha" | |
author: "jd" | |
url: "https://forum.farmanager.com/viewtopic.php?f=15&t=8802" | |
id: "515F75F6-071A-42FB-99BA-A21630C1F63E" | |
minfarversion:{3,0,0,6096,0} | |
--files: "*.cfg;*.ru.lng" | |
options: | |
Alt:"RAlt" -- supported: LAlt | RAlt | |
keepHighlight:true | |
HighlightMinLen:3 | |
--FoundMargin:5 -- center if not set | |
CtrlA:"far" -- "lfsearch" | "editfind" | function (str,options) | |
search: | |
ExtraQuick:true | |
--CaseSensitive:true | |
--Reverse:true | |
--WholeWords:true | |
useBM:false | |
msgdelay:300 | |
--disabled:true | |
} | |
return unless nfo and not nfo.disabled | |
_filename = ... | |
SearchAll = | |
far: (str,options) -> | |
Far.DisableHistory -1 | |
Keys "F7" | |
mf.print str | |
Keys "Down" | |
Keys options.CaseSensitive and "Add" or "Subtract" | |
Keys "Down" | |
Keys options.WholeWords and "Add" or "Subtract" | |
Keys "PgDn Right Enter EnOut" | |
lfsearch: (str,options) -> | |
Plugin.Call "8E11EA75-0303-4374-AC60-D1E38F865449","code", " | |
lfsearch.EditorAction('test:showall', { | |
sSearchPat=%q, | |
bCaseSens=#{options.CaseSensitive}, | |
bRegExpr=false, | |
bWholeWords=#{options.WholeWords}, | |
bHighlight=true, | |
}, true)"\format str | |
editfind: (str,options) -> | |
Plugin.Call "E4ABD267-C2F9-4158-818F-B0E040A2AB9F", | |
"CaseSensitive:%s WholeWords:%s Highlight:1 RegExp:0 Error:0 Grep #{str}"\format options.CaseSensitive and 1 or 0, | |
options.WholeWords and 1 or 0 | |
O = nfo.options | |
F = far.Flags | |
-- globals -- | |
Origin = {} | |
local BS, last, options | |
-- -- -- -- -- | |
gfind_plain = (text, str) -> | |
pos = 1 | |
-> | |
a, b = text\find str, pos, true | |
if a then | |
pos = (a==b+1 and a or b) + 1 | |
a, b, text\sub a,b | |
word_pattern = "[%w_]+" | |
word_pattern = "(()#{word_pattern}())" | |
gfind_word = (text, word) -> | |
gmatch = text\gmatch word_pattern | |
-> | |
while true do | |
w, a, b = gmatch! | |
unless w | |
return | |
elseif w==word | |
return a, b-1, w | |
color_guid = win.Uuid"CFD4FAA9-A2F3-4652-ABF7-0D87B88D7C2C" | |
color, color_cursor = 0xCF, 7 | |
if Far.GetConfig"Interface.VirtualTerminalRendering" | |
color_cursor = | |
BackgroundColor:F.ALPHAMASK +0, | |
Flags:F.FCF_INHERIT_STYLE +F.FCF_FG_BLINK | |
Active,Match = {},{} | |
hilite = (str, id) -> | |
{ :CaseSensitive, :WholeWords } = options | |
Match[id] = :CaseSensitive, :WholeWords, :str | |
Event | |
description:"IncSearch: highlight current match occurrences" | |
group:"EditorEvent" | |
action:(id,event)-> | |
switch event | |
when F.EE_CLOSE | |
Active[id] = nil | |
Match[id] = nil | |
when F.EE_REDRAW | |
with editor.GetInfo id | |
if Match[id] | |
for line=.TopScreenLine, math.min .TopScreenLine+.WindowSizeY-1, .TotalLines | |
if text = editor.GetString id,line,3 | |
str = Match[id].str | |
too_short = str\len! <O.HighlightMinLen | |
unless Match[id].CaseSensitive or too_short | |
text,str = text\lower!, str\lower! | |
gfind = (Match[id].WholeWords or too_short) and gfind_word or gfind_plain | |
for a,b in gfind text,str | |
editor.AddColor id, line, a, b, F.ECF_AUTODELETE, color, 100, color_guid | |
if Active[id]--Id | |
editor.AddColor id, .CurLine, .CurPos, .CurPos, F.ECF_AUTODELETE, color_cursor, 101, color_guid | |
nil | |
findNext = (str, gfind, case_sens, CurLine, CurPos, to, step) -> | |
for line=CurLine,to,step | |
local found | |
text = editor.GetString nil,line,3 | |
text = text\lower! unless case_sens | |
for a,b,capture in gfind text, case_sens and str or str\lower! | |
current = StartPos:a, EndPos:b, StartLine:line, :capture | |
if line==CurLine | |
if step==1 | |
continue if a<CurPos | |
else | |
continue if a>=CurPos --or b>=CurPos | |
found = current | |
if step==1 | |
break | |
if found | |
return found | |
tab = (string.char 26).." " | |
setStatus = (str,msg) -> | |
if str | |
names = {"Reverse", "ExtraQuick", "CaseSensitive", "WholeWords"} | |
opts = "RQCW"\gsub "()(.)",(i,ch)->not options[names[i]] and ch\lower! | |
str = (" │ %s │ IncSearch: "\format opts)..str\gsub(" ", "·")\gsub("\t", tab)..(msg or "") | |
editor.SetTitle nil, str | |
local processRec --fwd decl. | |
MessagePopup = (msg, flags, wait) -> | |
s = far.SaveScreen! | |
far.Message msg, nfo.name, "", flags | |
if wait | |
while true | |
win.Sleep 50 | |
rec = win.ExtractKeyEx! | |
break unless rec | |
processRec rec | |
else | |
win.Sleep O.msgdelay or 500 | |
far.RestoreScreen s | |
far.Text! | |
SetSelection = (sel) -> | |
if sel then with sel | |
unless .BlockStartPos | |
sel = | |
BlockType: F.BTYPE_STREAM | |
BlockStartPos: .StartPos | |
BlockWidth: .EndPos - .StartPos + 1 | |
BlockStartLine: .StartLine | |
BlockHeight: .EndLine and .EndLine - .StartLine + 1 or 1 | |
editor.Select nil, sel or F.BTYPE_NONE | |
search = (str, info, dir, shift, loop) -> | |
if str=="" | |
editor.Select nil,F.BTYPE_NONE | |
editor.SetPosition nil, Origin.pos | |
Match[Origin.pos.EditorID] = nil | |
editor.Redraw! | |
setStatus "" | |
return | |
with editor.GetInfo! | |
local pos,line,finish | |
unless loop | |
line = .CurLine | |
finish = dir==-1 and 1 or .TotalLines | |
pos = info and info.SelStart or .CurPos | |
pos += dir*shift | |
pos += 1 if dir==-1 | |
if pos==0 | |
line -= 1 | |
pos = math.huge | |
else | |
pos,line,finish = 1,1,.CurLine | |
if dir==-1 | |
pos,line = math.huge, .TotalLines | |
found = findNext str, | |
options.WholeWords and gfind_word or gfind_plain, options.CaseSensitive, | |
line, pos, finish, dir==-1 and -1 or 1 | |
if found | |
{capture:str} = found | |
hilite str, .EditorID | |
SetSelection found | |
CurLine,CurPos = found.StartLine, found.EndPos+1 | |
TopScreenLine = if CurLine<.TopScreenLine or CurLine>.TopScreenLine+.WindowSizeY-1 | |
math.max 1, | |
if dir==-1 | |
CurLine - (O.FoundMargin or .WindowSizeY/2) | |
else | |
CurLine - (.WindowSizeY-1) + (O.FoundMargin or .WindowSizeY/2) | |
LeftPos = do | |
right_tab_pos = editor.RealToTab nil, CurLine, | |
select 2,(editor.GetString nil, CurLine, 3)\find "%w*",CurPos-1 | |
math.max TopScreenLine and 1 or .LeftPos, | |
2 + editor.TabToReal nil, CurLine, right_tab_pos - (.WindowSizeX-1) | |
pos = :CurLine, :CurPos, :LeftPos, TopScreenLine: TopScreenLine or .TopScreenLine | |
editor.SetPosition nil, pos | |
--save pos in order to enable back step | |
pos.sel = found | |
BS[str] = pos | |
editor.Redraw! | |
setStatus str | |
else ----------------------- | |
pos,text = .CurTabPos, str | |
if info | |
if str==info.str | |
pos = editor.RealToTab nil, .CurLine, info.SelStart | |
elseif info.str==string.sub str,1,#info.str | |
text = string.sub str,#info.str+1,-1 | |
if loop=="loop" | |
MessagePopup "String not found: "..str,"w","wait" | |
else | |
far.Text pos - (editor.RealToTab nil, .CurLine, .LeftPos), | |
.CurLine-.TopScreenLine+1, | |
actl.GetColor far.Colors.COL_WARNDIALOGBOXTITLE, | |
text | |
far.Text! | |
setStatus str,": not found" | |
mf.beep! | |
win.Sleep 300 | |
if loop and loop~="loop" | |
search str, info, dir, shift, "loop" | |
editor.Redraw! | |
setStatus info and info.str or "" | |
toggleOption = (str,opt) -> | |
options[opt] = not options[opt] | |
setStatus str | |
MessagePopup opt..": "..(options[opt] and "on" or "off") | |
true, false | |
QUIT = {} | |
actions = | |
--skip | |
CtrlShift: -> Keys"CtrlShift" | |
RCtrlShift: -> Keys"RCtrlShift" | |
Shift: -> Keys"Shift" | |
F18: -> Keys"F18" | |
--backspace/restore | |
BS: (str)-> | |
return last if str=="" | |
str = str\lower! unless options.CaseSensitive | |
BS[str] = nil | |
str = str\sub 1, -2 | |
if ei = BS[str] --back step | |
editor.SetPosition nil, ei | |
SetSelection ei.sel | |
editor.Redraw! | |
far.Text! | |
setStatus str | |
return -- no search needed | |
str, false | |
--clear/restore | |
CtrlBS: (str)-> | |
BS = {} | |
last if str=="" else "" | |
--insert | |
ShiftIns: (str)-> | |
if clip = far.PasteFromClipboard!\match"[^\r\n]+" | |
str..clip | |
--quit and restore pos | |
Esc: -> | |
unless O.useBM | |
editor.SetPosition nil, Origin.pos | |
SetSelection Origin.sel | |
QUIT | |
--quit | |
Alt: -> QUIT | |
--next | |
ShiftF7: (str)-> | |
str=="" and last or str, 1 | |
--prev | |
AltF7: (str)-> | |
str=="" and last or str, -1 | |
CtrlA: (str)-> | |
str=="" and last or str | |
unless str=="" | |
SearchAll[O.CtrlA] if SearchAll[O.CtrlA] else O.CtrlA | |
AltQ: (str)-> toggleOption str,"ExtraQuick" | |
AltR: (str)-> toggleOption str,"Reverse" | |
AltC: (str)-> toggleOption str,"CaseSensitive" | |
AltW: (str)-> toggleOption str,"WholeWords" | |
AltRight: (str)-> with editor.GetString! | |
pos = .SelStart~=0 and .SelEnd+1 or editor.GetInfo!.CurPos | |
word = .StringText\match "^%w+", pos | |
return str..(word or .StringText\sub pos, pos), false | |
F1: -> | |
far.ShowHelp _filename, nil, F.FHELP_CUSTOMFILE | |
far.Text! | |
nil | |
xform = | |
CtrlV: "ShiftIns"--insert | |
RAlt: "Alt" --quit | |
CtrlRAlt: "Alt" --quit (AltGr) | |
--CtrlI: "Alt" --quit | |
F3: "ShiftF7" --next | |
Enter: "ShiftF7" | |
RCtrl: "ShiftF7" | |
ShiftF3: "AltF7" --prev | |
ShiftEnter:"AltF7" | |
Ctrl: "AltF7" | |
RCtrlBS: "CtrlBS" | |
RAltRight: "AltRight" | |
local lastdown, lastVK,ignore | |
processRec = (rec)-> | |
if rec.EventType==F.KEY_EVENT | |
name = far.InputRecordToName rec | |
vk,down = rec.VirtualKeyCode, rec.KeyDown | |
if down | |
if vk==lastVK --autorepeat | |
if vk==ignore | |
return nil --eat | |
else -- yield key one more time with additional "force" arg | |
ignore = vk | |
if not name | |
rec.KeyDown = false | |
name = far.InputRecordToName rec | |
return unless xform[name] | |
return name, vk | |
lastVK = vk | |
else | |
lastVK = nil | |
name = nil if vk==ignore | |
ignore = nil | |
if name and not down | |
name = nil unless lastdown | |
lastdown = not name and down | |
name | |
-- todo: should depend on kbd layout | |
ALTGR = F.LEFT_CTRL_PRESSED + F.RIGHT_ALT_PRESSED | |
MODS = ALTGR + F.RIGHT_CTRL_PRESSED + F.LEFT_ALT_PRESSED | |
INTERVAL = 20 | |
waitKey = -> | |
while true | |
if rec = win.ExtractKeyEx! | |
name,force = processRec rec | |
if name | |
with rec | |
char = do | |
if .UnicodeChar and (.UnicodeChar=="\t" or .UnicodeChar>=" ") | |
mods = bit64.band .ControlKeyState, MODS | |
if mods==0 or .UnicodeChar~=" " and mods==ALTGR | |
.UnicodeChar | |
return name, char, force | |
win.Sleep INTERVAL | |
pickSel = -> | |
with info = editor.GetString nil, 0, 0 | |
if .SelStart>0 | |
.str = .StringText\sub .SelStart, .SelEnd | |
return .str, info | |
"" | |
IncSearch = (arg) -> | |
options = require"moon".copy O.search | |
BM.Add! if O.useBM | |
local str,picked | |
with sel = editor.GetSelection! | |
Origin.sel = sel | |
if arg.str | |
str = arg.str | |
elseif sel and .StartLine==.EndLine --single-line selection only | |
str,picked = pickSel! | |
with ei = editor.GetInfo! | |
Active[.EditorID] = true | |
Origin.pos = ei | |
hilite str, .EditorID | |
BS = {} | |
search str or "", picked, arg.dir or 0, arg.dir and 1 or 0 | |
lastVK = nil | |
while true | |
str,picked = pickSel! | |
key, char, force = waitKey! | |
if key=="quit" | |
break | |
elseif char | |
search str..char, picked, | |
options.Reverse and -1 or 1, | |
options.ExtraQuick and 1 or 0, | |
force | |
elseif action = actions[xform[key] or key] | |
new_str, dir = action str | |
if new_str | |
if new_str==QUIT | |
break | |
elseif type(new_str)=="function" | |
mf.postmacro new_str,str,options | |
break | |
str = new_str unless new_str==true | |
search str, picked, | |
dir or options.Reverse and -1 or 1, | |
dir~=false and options.ExtraQuick and str~="" and 1 or 0, | |
force | |
else | |
mf.postmacro -> | |
if -2==mf.eval key, 2 | |
Keys key | |
break | |
with Origin.pos | |
Active[.EditorID] = nil | |
Match[.EditorID] = nil unless O.keepHighlight | |
setStatus! | |
last = str unless str=="" | |
Macro | |
description: "incremental search mode" | |
area: "Editor" | |
key: O.Alt -- https://forum.farmanager.com/viewtopic.php?p=180768#p180768 | |
id: "9CDB70B8-2774-491C-9F7C-46B4B0BC14CE" | |
action: -> IncSearch{} | |
AKey = -> mf.akey 1, 1 | |
Macro | |
description: "incremental search: continue" | |
area: "Editor" | |
key: "Ctrl"..O.Alt | |
id: "DA1821EA-2C4B-415D-8462-42014A8186F9" | |
condition: -> Object.Selected and 60 or 40 | |
action: -> | |
IncSearch | |
str: not Object.Selected and last | |
dir: AKey!\match"^Ctrl" and -1 or 1 | |
shift: 1 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment