Skip to content

Instantly share code, notes, and snippets.

@johnd0e
Last active June 29, 2025 22:11
Show Gist options
  • Save johnd0e/ee66b38aae026a8ecef4b36cd45c036a to your computer and use it in GitHub Desktop.
Save johnd0e/ee66b38aae026a8ecef4b36cd45c036a to your computer and use it in GitHub Desktop.
[FAR macro] Incremental search in editor
--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
 "
.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