fy36
2025-05-14 a37aca60ff9914b0abb710f04118b22420f4f398
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
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
-- Copyright 2011-18 Paul Kulchenko, ZeroBrane LLC
---------------------------------------------------------
 
local ide = ide
local q = EscapeMagic
local unpack = table.unpack or unpack
 
local dc = wx.wxMemoryDC()
local function getFontHeight(font)
  dc:SetFont(font)
  local _, h = dc:GetTextExtent("AZ")
  dc:SetFont(wx.wxNullFont)
  return h
end
 
local pending
local function pendingInput()
  if ide.osname ~= 'Unix' then
    ide:GetApp():SafeYieldFor(wx.NULL, wx.wxEVT_CATEGORY_USER_INPUT + wx.wxEVT_CATEGORY_UI)
  end
  return pending
end
local showProgress
local function showCommandBar(params)
  local onDone, onUpdate, onItem, onSelection, defaultText, selectedText =
    params.onDone, params.onUpdate, params.onItem, params.onSelection,
    params.defaultText, params.selectedText
  local row_width = ide.config.commandbar.width or 0
  if row_width < 1 then
    row_width = math.max(450, math.floor(row_width * ide:GetMainFrame():GetClientSize():GetWidth()))
  end
 
  local maxlines = ide.config.commandbar.maxlines
  local lines = {}
  local linenow = 0
 
  local sash = ide:GetUIManager():GetArtProvider():GetMetric(wxaui.wxAUI_DOCKART_SASH_SIZE)
  local border = sash + 2
 
  local nb = ide:GetEditorNotebook()
  local pos = nb:GetScreenPosition()
  if pos then
    local minx, miny
    for p = 0, nb:GetPageCount()-1 do
      local sp = nb:GetPage(p):GetScreenPosition()
      local x, y = sp:GetX(), sp:GetY()
      -- just in case, compare with the position of the notebook itself;
      -- this is needed because the tabs that haven't been refreshed yet
      -- may report 0 as their screen position on Linux, which is incorrect.
      if y > pos:GetY() and (not miny or y < miny) then miny = y end
      if x > pos:GetX() and (not minx or x < minx) then minx = x end
    end
    local anchorx = pos:GetX()+nb:GetClientSize():GetWidth()-row_width-16
    local cp = nb:GetCurrentPage()
    if cp and cp:GetScreenPosition():GetX() ~= minx then
      anchorx = pos:GetX()+border
    end
    pos:SetX(anchorx)
    pos:SetY((miny or pos:GetY())+2)
  else
    pos = wx.wxDefaultPosition
  end
 
  local tempctrl = ide:IsValidCtrl(ide:GetProjectTree()) and ide:GetProjectTree() or wx.wxTreeCtrl()
  local tfont = tempctrl:GetFont()
  local ffont = (ide:GetEditor() or ide:CreateBareEditor()):GetFont()
  ffont:SetPointSize(ffont:GetPointSize()+2)
  local sfont = wx.wxFont(tfont)
  tfont:SetPointSize(tfont:GetPointSize()+2)
 
  local hoffset = 4
  local voffset = 2
 
  local line_height = getFontHeight(ffont)
  local row_height = line_height + getFontHeight(sfont) + voffset * 3 -- before, after, and between
 
  local frame = wx.wxFrame(ide:GetMainFrame(), wx.wxID_ANY, "Command Bar",
    pos, wx.wxDefaultSize,
    wx.wxFRAME_NO_TASKBAR + wx.wxFRAME_FLOAT_ON_PARENT + wx.wxNO_BORDER)
  local panel = wx.wxPanel(frame or ide:GetMainFrame(), wx.wxID_ANY,
    wx.wxDefaultPosition, wx.wxDefaultSize, wx.wxFULL_REPAINT_ON_RESIZE)
  local search = wx.wxTextCtrl(panel, wx.wxID_ANY, "\1",
    wx.wxDefaultPosition,
    -- make the text control proportional to the font size
    wx.wxSize(row_width, getFontHeight(tfont) + voffset),
    wx.wxTE_PROCESS_ENTER + wx.wxTE_PROCESS_TAB + wx.wxNO_BORDER)
  local results = wx.wxScrolledWindow(panel, wx.wxID_ANY,
    wx.wxDefaultPosition, wx.wxSize(0, 0))
 
  local style, styledef = ide.config.styles, StylesGetDefault()
  local textcolor = wx.wxColour(unpack(style.text.fg or styledef.text.fg))
  local backcolor = wx.wxColour(unpack(style.text.bg or styledef.text.bg))
  local selcolor = wx.wxColour(unpack(style.caretlinebg.bg or styledef.caretlinebg.bg))
  local pancolor = ide:GetUIManager():GetArtProvider():GetColour(wxaui.wxAUI_DOCKART_SASH_COLOUR)
  local borcolor = ide:GetUIManager():GetArtProvider():GetColour(wxaui.wxAUI_DOCKART_BORDER_COLOUR)
 
  search:SetBackgroundColour(backcolor)
  search:SetForegroundColour(textcolor)
  search:SetFont(tfont)
 
  local nbrush = wx.wxBrush(backcolor, wx.wxSOLID)
  local sbrush = wx.wxBrush(selcolor, wx.wxSOLID)
  local bbrush = wx.wxBrush(pancolor, wx.wxSOLID)
  local lpen = wx.wxPen(borcolor, 1, wx.wxDOT)
  local bpen = wx.wxPen(borcolor, 1, wx.wxSOLID)
  local npen = wx.wxPen(backcolor, 1, wx.wxSOLID)
 
  local topSizer = wx.wxFlexGridSizer(2, 1, -border*2, 0)
  topSizer:SetFlexibleDirection(wx.wxVERTICAL)
  topSizer:AddGrowableRow(1, 1)
  topSizer:Add(search, wx.wxSizerFlags(0):Expand():Border(wx.wxALL, border))
  topSizer:Add(results, wx.wxSizerFlags(1):Expand():Border(wx.wxALL, border))
  panel:SetSizer(topSizer)
  topSizer:Fit(frame) -- fit the frame/panel around the controls
 
  local minheight = frame:GetClientSize():GetHeight()
 
  -- make a one-time callback;
  -- needed because KILL_FOCUS handler can be called after closing window
  local function onExit(index)
    -- delay destroying the frame until all the related processing is done
    ide:DoWhenIdle(function() if ide:IsValidCtrl(frame) then frame:Destroy() end end)
 
    onExit = function() end
    onDone(index and lines[index], index, search:GetValue())
  end
 
  local linesnow
  local function onPaint(event)
    if not ide:IsValidCtrl(frame) then return end
 
    -- adjust the scrollbar before working with the canvas
    local _, starty = results:GetViewStart()
    -- recalculate the scrollbars if the number of lines shown has changed
    if #lines ~= linesnow then
      -- adjust the starting line when the current line is the last one
      if linenow > starty+maxlines then starty = starty + 1 end
      results:SetScrollbars(1, row_height, 1, #lines, 0, starty*row_height, false)
      linesnow = #lines
    end
 
    local dc = wx.wxMemoryDC(results)
    results:PrepareDC(dc)
 
    local size = results:GetVirtualSize()
    local w,h = size:GetWidth(),size:GetHeight()
    local bitmap = wx.wxBitmap(w,h)
    local scale = ide:GetContentScaleFactor()
    -- scale the bitmap before drawing
    if ide:IsValidProperty(bitmap, "CreateScaled") and scale > 1 then
      bitmap:CreateScaled(w, h, bitmap:GetDepth(), scale)
    end
    dc:SelectObject(bitmap)
 
    -- clear the background
    dc:SetBackground(nbrush)
    dc:Clear()
 
    dc:SetTextForeground(textcolor)
    dc:SetBrush(sbrush)
    for r = 1, #lines do
      if r == linenow then
        dc:SetPen(wx.wxTRANSPARENT_PEN)
        dc:DrawRectangle(0, row_height*(r-1), row_width, row_height+1)
      end
      dc:SetPen(lpen)
      dc:DrawLine(hoffset, row_height*(r-1), row_width-hoffset*2, row_height*(r-1))
 
      local fline, sline = onItem(lines[r])
      if fline then
        dc:SetFont(ffont)
        dc:DrawText(fline, hoffset, row_height*(r-1)+voffset)
      end
      if sline then
        dc:SetFont(sfont)
        dc:DrawText(sline, hoffset, row_height*(r-1)+line_height+voffset*2)
      end
    end
 
    dc:SetPen(wx.wxNullPen)
    dc:SetBrush(wx.wxNullBrush)
    dc:SelectObject(wx.wxNullBitmap)
    dc:delete()
 
    dc = wx.wxPaintDC(results)
    dc:DrawBitmap(bitmap, 0, 0, true)
    dc:delete()
  end
 
  local progress = 0
  showProgress = function(newprogress)
    progress = newprogress
    if not ide:IsValidCtrl(panel) then return end
    panel:Refresh()
    panel:Update()
  end
 
  local function onPanelPaint(event)
    if not ide:IsValidCtrl(frame) then return end
 
    local dc = wx.wxBufferedPaintDC(panel)
    dc:SetBrush(bbrush)
    dc:SetPen(bpen)
 
    local psize = panel:GetClientSize()
    dc:DrawRectangle(0, 0, psize:GetWidth(), psize:GetHeight())
    dc:DrawRectangle(sash+1, sash+1, psize:GetWidth()-2*(sash+1), psize:GetHeight()-2*(sash+1))
 
    if progress > 0 then
      dc:SetBrush(nbrush)
      dc:SetPen(npen)
      dc:DrawRectangle(sash+2, 1, math.floor((row_width-4)*progress), sash)
    end
 
    dc:SetPen(wx.wxNullPen)
    dc:SetBrush(wx.wxNullBrush)
    dc:delete()
  end
 
  local linewas -- line that was reported when updated
  local function onTextUpdated()
    if ide:IsValidProperty(ide:GetApp(), "GetMainLoop") then
      pending = ide:GetApp():GetMainLoop():IsYielding()
    end
    if pending then return end
 
    local text = search:GetValue()
    lines = onUpdate(text)
    linenow = #text > 0 and #lines > 0 and 1 or 0
    linewas = nil
 
    -- the control can disappear during onUpdate as it can be closed, so check for that
    if not ide:IsValidCtrl(frame) then return end
 
    local size = frame:GetClientSize()
    local height = minheight + row_height*math.min(maxlines,#lines)
    if height ~= size:GetHeight() then
      results:SetScrollbars(1, 1, 1, 1, 0, 0, false)
      size:SetHeight(height)
      frame:SetClientSize(size)
    end
 
    results:Refresh()
  end
 
  local function onKeyDown(event)
    if ide:IsValidProperty(ide:GetApp(), "GetMainLoop")
    and ide:GetApp():GetMainLoop():IsYielding() then
      event:Skip()
      return
    end
 
    local linesnow = #lines
    local keycode = event:GetKeyCode()
    if keycode == wx.WXK_RETURN then
      onExit(linenow)
      return
    elseif event:GetModifiers() ~= wx.wxMOD_NONE then
      event:Skip()
      return
    elseif keycode == wx.WXK_UP then
      if linesnow > 0 then
        linenow = linenow - 1
        if linenow <= 0 then linenow = linesnow end
      end
    elseif keycode == wx.WXK_DOWN then
      if linesnow > 0 then
        linenow = linenow % linesnow + 1
      end
    elseif keycode == wx.WXK_PAGEDOWN then
      if linesnow > 0 then
        linenow = linenow + maxlines
        if linenow > linesnow then linenow = linesnow end
      end
    elseif keycode == wx.WXK_PAGEUP then
      if linesnow > 0 then
        linenow = linenow - maxlines
        if linenow <= 0 then linenow = 1 end
      end
    elseif keycode == wx.WXK_ESCAPE then
      onExit(false)
      return
    else
      event:Skip()
      return
    end
 
    local _, starty = results:GetViewStart()
    if linenow < starty+1 then results:Scroll(-1, linenow-1)
    elseif linenow > starty+maxlines then results:Scroll(-1, linenow-maxlines) end
    results:Refresh()
  end
 
  local function onMouseLeftDown(event)
    local pos = event:GetPosition()
    local _, y = results:CalcUnscrolledPosition(pos.x, pos.y)
    onExit(math.floor(y / row_height)+1)
  end
 
  local function onIdle(event)
    if pending then return onTextUpdated() end
    if linewas == linenow then return end
    linewas = linenow
    if linenow == 0 then return end
 
    -- save the selection/insertion point as it's reset on Linux (wxwidgets 2.9.5)
    local ip = search:GetInsertionPoint()
    local f, t = search:GetSelection()
 
    -- this may set focus to a different object/tab,
    -- so disable the focus event and then set the focus back
    search:SetEvtHandlerEnabled(false)
    onSelection(lines[linenow], search:GetValue())
    search:SetFocus()
    search:SetEvtHandlerEnabled(true)
    if ide.osname == 'Unix' then
      search:SetInsertionPoint(ip)
      search:SetSelection(f, t)
    end
  end
 
  panel:Connect(wx.wxEVT_PAINT, onPanelPaint)
  panel:Connect(wx.wxEVT_ERASE_BACKGROUND, function() end)
  panel:Connect(wx.wxEVT_IDLE, onIdle)
 
  results:Connect(wx.wxEVT_PAINT, onPaint)
  results:Connect(wx.wxEVT_LEFT_DOWN, onMouseLeftDown)
  results:Connect(wx.wxEVT_ERASE_BACKGROUND, function() end)
 
  search:Connect(wx.wxEVT_KEY_DOWN, onKeyDown)
  search:Connect(wx.wxEVT_COMMAND_TEXT_UPDATED, onTextUpdated)
  search:Connect(wx.wxEVT_COMMAND_TEXT_ENTER, function() onExit(linenow) end)
  -- this could be done with calling `onExit`, but on OSX KILL_FOCUS is called before
  -- mouse LEFT_DOWN, which closes the panel before the results are taken;
  -- to avoid this, `onExit` call is delayed and handled in IDLE event
  search:Connect(wx.wxEVT_KILL_FOCUS, function() onExit() end)
 
  frame:Show(true)
  frame:Update()
  frame:Refresh()
 
  search:SetValue((defaultText or "")..(selectedText or ""))
  search:SetSelection(#(defaultText or ""), -1)
  search:SetFocus()
end
 
local sep = "[/\\%-_ ]+"
local weights = {onegram = 0.1, digram = 0.4, trigram = 0.5}
local cache = {}
local missing = 3 -- penalty for missing symbols (1 missing == N matching)
local casemismatch = 0.9 -- score for case mismatch (%% of full match)
local function score(p, v)
  local function ngrams(str, num, low, needcache)
    local key = str..(low and '\1' or '\2')..num
    if cache[key] then return unpack(cache[key]) end
 
    local t, l, p = {}, {}, 0
    for i = 1, #str-num+1 do
      local pair = str:sub(i, i+num-1)
      p = p + (t[pair] and 0 or 1)
      if low and pair:find('%u') then l[pair:lower()] = casemismatch end
      t[pair] = 1
    end
    if needcache then cache[key] = {t, p, l} end
    return t, p, l
  end
 
  local function overlap(pattern, value, num)
    local ph, ps = ngrams(pattern, num, false, true)
    local vh, vs, vl = ngrams(value, num, true)
    if ps + vs == 0 then return 0 end
 
    local is = 0 -- intersection of two sets of ngrams
    for k in pairs(ph) do is = is + (vh[k] or vl[k:lower()] or 0) end
    return is / (ps + vs) - (num == 1 and missing * (ps - is) / (ps + vs) or 0)
  end
 
  local key = p..'\3'..v
  if not cache[key] then
    -- ignore all whitespaces in the pattern for one-gram comparison
    local score = weights.onegram * overlap(p:gsub("%s+",""), v, 1)
    if score > 0 then -- don't bother with those that can't even score 1grams
      p = ' '..(p:gsub(sep, ' '))
      v = ' '..(v:gsub(sep, ' '))
      score = score + weights.digram * overlap(p, v, 2)
      score = score + weights.trigram * overlap(' '..p, ' '..v, 3)
    end
    cache[key] = 2 * 100 * score
  end
  return cache[key]
end
 
local function commandBarScoreItems(t, pattern, limit)
  local r, plen = {}, #(pattern:gsub("%s+",""))
  local maxp = 0
  local num = 0
  local total = #t
  local prefilter = ide.config.commandbar and tonumber(ide.config.commandbar.prefilter)
  -- anchor for 1-2 symbol patterns to speed up search
  local needanchor = prefilter and prefilter * 4 <= #t and plen <= 2
  local pref = pattern:sub(1,4):lower()
  local filter = prefilter and prefilter <= #t
    -- expand `abc` into `a.*b.*c`, but limit the prefix to avoid penalty for `s.*s.*s.*....`
    -- if there are too many records to filter, then only search for substrings
    and (prefilter * 10 <= #t and q(pref):gsub("%s+",".")
      or pref:gsub("%s",""):gsub(".", function(s) return q(s)..".*" end):gsub("%.%*$",""))
    or nil
  local lastpercent = 0
  for n, v in ipairs(t) do
    -- there was additional input while scoring, so abort to check for it
    local timeToCheck = n % ((prefilter or 250) * 10) == 0
    if timeToCheck and pendingInput() then r = {}; break end
    local progress = n/total
    local percent = math.floor(progress * 100 + 0.5)
    if timeToCheck and percent ~= lastpercent then
      lastpercent = percent
      if showProgress then showProgress(progress) end
    end
 
    if #v >= plen then
      local match = filter and v:lower():find(filter)
      -- check if the current name needs to be prefiltered or anchored (for better performance);
      -- if it needs to be anchored, then anchor it at the beginning of the string or the word
      if not filter or (match and (not needanchor or match == 1 or v:find("^[%p%s]", match-1))) then
        local p = math.floor(score(pattern, v))
        maxp = math.max(p, maxp)
        if p > 1 and p > maxp / 4 then
          num = num + 1
          r[num] = {v, p}
        end
      end
    end
  end
  table.sort(r, function(a, b) return a[2] > b[2] end)
  -- limit the list to be displayed
  -- `r[limit+1] = nil` is not desired as the resulting table may be sorted incorrectly
  if tonumber(limit) and limit < #r then
    local tmp = r
    r = {}
    for i = 1, limit do r[i] = tmp[i] end
  end
  if showProgress then showProgress(0) end
  return r
end
 
local markername = "commandbar.background"
local mac = ide.osname == 'Macintosh'
local win = ide.osname == 'Windows'
local special = {SYMBOL = '@', LINE = ':', METHOD = ';'}
local tabsep = "\0"
local function name2index(name)
  local p = name:find(tabsep)
  return p and tonumber(name:sub(p + #tabsep)) or nil
end
local files
function ShowCommandBar(default, selected)
  local styles = ide.config.styles
  -- re-register the marker as the colors might have changed
  local marker = ide:AddMarker(markername,
    wxstc.wxSTC_MARK_BACKGROUND, styles.text.fg, styles.caretlinebg.bg)
 
  local nb = ide:GetEditorNotebook()
  local selection = nb:GetSelection()
  local maxitems = ide.config.commandbar.maxitems
  local preview, origline, functions, methods
 
  local function markLine(ed, toline)
    ed:MarkerDefine(ide:GetMarker(markername))
    ed:MarkerDeleteAll(marker)
    ed:MarkerAdd(toline-1, marker)
    -- store the original line if not stored yet
    origline = origline or (ed:GetCurrentLine()+1)
    ed:EnsureVisibleEnforcePolicy(toline-1)
  end
 
  showCommandBar({
    defaultText = default or "",
    selectedText = selected or "",
    onDone = function(t, enter, text)
      if not mac then nb:Freeze() end
 
      -- delete all current line markers if any; restore line position
      local ed = ide:GetEditor()
      if ed and origline then
        ed:MarkerDeleteAll(marker)
        -- only restore original line if Escape was used (enter == false)
        if enter == false then ed:EnsureVisibleEnforcePolicy(origline-1) end
      end
 
      if enter then
        local fline, sline, docindex = unpack(t or {})
 
        -- jump to symbol; docindex has the position of the symbol
        if text and text:find(special.SYMBOL) then
          if sline and docindex then
            local index = name2index(sline)
            local editor = index and nb:GetPage(index):DynamicCast("wxStyledTextCtrl")
            if not editor then
              local doc = ide:FindDocument(sline)
              -- reload the file (including the preview to refresh its symbols in the outline)
              editor = LoadFile(sline, (not doc or doc:GetEditor() == preview) and preview or nil)
            end
            if editor then
              if preview and preview ~= editor then ide:GetDocument(preview):Close() end
              editor:GotoPos(docindex-1)
              editor:EnsureVisibleEnforcePolicy(editor:LineFromPosition(docindex-1))
              ide:DoWhenIdle(function() ide:GetDocument(editor):SetActive() end)
            end
          end
        -- insert selected method
        elseif text and text:find('^%s*'..special.METHOD) then
          if ed then -- clean up text and insert at the current location
            local method = sline
            local isfunc = methods.desc[method][1]:find(q(method).."%s*%(")
            local text = method .. (isfunc and "()" or "")
            local pos = ed:GetCurrentPos()
            ed:InsertTextDyn(pos, text)
            ed:EnsureVisibleEnforcePolicy(ed:LineFromPosition(pos))
            ed:GotoPos(pos + #method + (isfunc and 1 or 0))
            if isfunc then -- show the tooltip
              local frame = ide:GetMainFrame()
              frame:SetFocus()
              frame:AddPendingEvent(
                wx.wxCommandEvent(wx.wxEVT_COMMAND_MENU_SELECTED, ID.SHOWTOOLTIP))
            end
          end
        -- set line position in the (current) editor if requested
        elseif text and text:find(special.LINE..'(%d*)%s*$') then
          local toline = tonumber(text:match(special.LINE..'(%d+)'))
          if toline and ed then
            ed:GotoLine(toline-1)
            ed:EnsureVisibleEnforcePolicy(toline-1)
            ide:DoWhenIdle(function() ide:GetDocument(ed):SetActive() end)
          end
        elseif docindex then -- switch to existing document
          local doc = ide:GetDocumentList()[docindex]
          if preview and preview ~= doc:GetEditor() then ide:GetDocument(preview):Close() end
          -- delay switching to allow the panel to be destroyed, as it may pull the focus away
          ide:DoWhenIdle(function() doc:SetActive() end)
        -- load a new file (into preview if set)
        elseif sline or text then
          -- 1. use "text" if Ctrl/Cmd-Enter is used
          -- 2. otherwise use currently selected file
          -- 3. otherwise use "text"
          local file = (wx.wxGetKeyState(wx.WXK_CONTROL) and text) or sline or text
          local fullPath = MergeFullPath(ide:GetProject(), file)
          local doc = ide:FindDocument(fullPath)
          -- if the document is already opened (not in the preview)
          -- or can't be opened as a file or folder, then close the preview
          if doc and doc:GetEditor() ~= preview
          or not LoadFile(fullPath, preview or nil) and not ide:SetProject(fullPath) then
            if preview then ide:GetDocument(preview):Close() end
          end
        end
      else
        -- close preview
        if preview then ide:GetDocument(preview):Close() end
        -- restore original selection if canceled
        if nb:GetSelection() ~= selection then nb:SetSelection(selection) end
      end
      preview = nil
      if not mac then nb:Thaw() end
 
      -- reset file cache if it's not needed
      if not ide.config.commandbar.filecache then files = nil end
    end,
    onUpdate = function(text)
      local lines = {}
      local projdir = ide:GetProject()
 
      -- delete all current line markers if any
      -- restore the original position if search text is updated
      local ed = ide:GetEditor()
      if ed and origline then ed:MarkerDeleteAll(marker) end
 
      -- reset cached functions if no symbol search
      if text and not text:find(special.SYMBOL) then
        functions = nil
        if ed and origline then ed:EnsureVisibleEnforcePolicy(origline-1) end
      end
      -- reset cached methods if no method search
      if text and not text:find(special.METHOD) then methods = nil end
 
      if text and text:find(special.SYMBOL) then
        local file, symbol = text:match('^(.*)'..special.SYMBOL..'(.*)')
        if not functions then
          local nums, paths = {}, {}
          functions = {pos = {}, src = {}}
 
          local function populateSymbols(path, symbols)
            for _, func in ipairs(symbols) do
              table.insert(functions, func.name)
              nums[func.name] = (nums[func.name] or 0) + 1
              local num = nums[func.name]
              functions.src[func.name..num] = path
              functions.pos[func.name..num] = func.pos
            end
          end
 
          local currentonly = #file > 0 and ed
          local outline = ide:GetOutline()
          for _, doc in pairs(currentonly and {ide:GetDocument(ed)} or ide:GetDocuments()) do
            local path, editor = doc:GetFilePath(), doc:GetEditor()
            if path then paths[path] = true end
            local index = doc:GetTabIndex()
            populateSymbols(path or doc:GetFileName()..tabsep..index, outline:GetEditorSymbols(editor))
          end
 
          -- now add all other files in the project
          if not currentonly and ide.config.commandbar.showallsymbols then
            local n = 0
            outline:RefreshSymbols(projdir, function(path)
                local symbols = outline:GetFileSymbols(path)
                if not paths[path] and symbols then populateSymbols(path, symbols) end
                if not symbols then n = n + 1 end
              end)
            if n > 0 then ide:SetStatusFor(TR("Queued %d files to index."):format(n)) end
          end
        end
        local nums = {}
        if #symbol > 0 then
          local topscore
          for _, item in ipairs(commandBarScoreItems(functions, symbol, maxitems)) do
            local func, score = unpack(item)
            topscore = topscore or score
            nums[func] = (nums[func] or 0) + 1
            local num = nums[func]
            if score > topscore / 4 and score > 1 then
              table.insert(lines, {("%2d %s"):format(score, func),
                  functions.src[func..num], functions.pos[func..num]})
            end
          end
        else
          for n, name in ipairs(functions) do
            if n > maxitems then break end
            nums[name] = (nums[name] or 0) + 1
            local num = nums[name]
            lines[n] = {name, functions.src[name..num], functions.pos[name..num]}
          end
        end
      elseif ed and text and text:find('^%s*'..special.METHOD) then
        if not methods then
          methods = {desc = {}}
          local num = 1
          if ed.api and ed.api.tip and ed.api.tip.shortfinfoclass then
            for libname, lib in pairs(ed.api.tip.shortfinfoclass) do
              for method, val in pairs(lib) do
                local signature, desc = val:match('(.-)\n(.*)')
                local m = (libname > "" and libname..'.' or "")..method
                desc = desc and desc:gsub("\n", " ") or val
                methods[num] = m
                methods.desc[m] = {signature or m, desc}
                num = num + 1
              end
            end
          end
        end
        local method = text:match(special.METHOD..'(.*)')
        if #method > 0 then
          local topscore
          for _, item in ipairs(commandBarScoreItems(methods, method, maxitems)) do
            local method, score = unpack(item)
            topscore = topscore or score
            if score > topscore / 4 and score > 1 then
              table.insert(lines, { score, method })
            end
          end
        end
      elseif text and text:find(special.LINE..'(%d*)%s*$') then
        local toline = tonumber(text:match(special.LINE..'(%d+)'))
        if toline and ed then markLine(ed, toline) end
      elseif text and #text > 0 and projdir and #projdir > 0 then
        -- populate the list of files
        files = files or ide:GetFileList(projdir, true, "*",
          {sort = false, path = false, folder = false, skipbinary = true})
        local topscore
        for _, item in ipairs(commandBarScoreItems(files, text, maxitems)) do
          local file, score = unpack(item)
          topscore = topscore or score
          if score > topscore / 4 and score > 1 then
            table.insert(lines, {
                ("%2d %s"):format(score, wx.wxFileName(file):GetFullName()),
                file,
            })
          end
        end
      else
        for index, doc in pairs(ide:GetDocumentList()) do
          lines[index] = {doc:GetFileName(), doc:GetFilePath(), index}
        end
      end
      return lines
    end,
    onItem = function(t)
      if methods then
        local score, method = unpack(t)
        return ("%2d %s"):format(score, methods.desc[method][1]), methods.desc[method][2]
      else
        return unpack(t)
      end
    end,
    onSelection = function(t, text)
      local _, file, docindex = unpack(t)
      local pos
      if text and text:find(special.SYMBOL) then
        pos, docindex = docindex, name2index(file)
      elseif text and text:find(special.METHOD) then
        return
      end
 
      if file then file = MergeFullPath(ide:GetProject(), file) end
      -- disabling event handlers for the notebook and the editor
      -- to minimize changes in the UI when editors are switched
      -- or files in the preview are updated.
      nb:SetEvtHandlerEnabled(false)
      local doc = file and ide:FindDocument(file)
      if docindex or doc then
        local doc = docindex and ide:GetDocumentList()[docindex] or doc
        local index, nb = doc:GetTabIndex()
        local ed = nb:GetPage(index)
        ed:SetEvtHandlerEnabled(false)
        if nb:GetSelection() ~= index then nb:SetSelection(index) end
        ed:SetEvtHandlerEnabled(true)
      elseif file then
        -- skip binary files with unknown extensions
        if #ide:GetKnownExtensions(GetFileExt(file)) > 0
        -- file may not be read if there is an error, so provide a default for that case
        or not IsBinary(FileRead(file, 2048) or "") then
          preview = preview or NewFile()
          preview:SetEvtHandlerEnabled(false)
          LoadFile(file, preview, true, true)
          preview:SetFocus()
          -- force refresh since the panel covers the editor on OSX/Linux
          -- this fixes the preview window not always redrawn on Linux
          if not win then preview:Update() preview:Refresh() end
          preview:SetEvtHandlerEnabled(true)
        elseif preview then
          ide:GetDocument(preview):Close()
          preview = nil
        end
      end
      nb:SetEvtHandlerEnabled(true)
 
      if text and text:find(special.SYMBOL) then
        local ed = ide:GetEditor()
        if ed then markLine(ed, ed:LineFromPosition(pos-1)+1) end
      end
    end,
  })
end
 
ide.test.commandBarScoreItems = commandBarScoreItems
 
local fsep = GetPathSeparator()
local function relpath(path, filepath)
  local pathpatt = "^"..EscapeMagic(path:gsub("[\\/]$",""):gsub("[\\/]", fsep))..fsep.."?"
  return (filepath:gsub(pathpatt, ""))
end
 
local addremove = {}
ide:AddPackage('core.commandbar', {
    onProjectLoad = function()
      cache = {} -- reset ngram cache when switching projects to conserve memory
      files = nil -- reset files cache when switching projects
    end,
    onFiletreeFileAdd =  function(self, tree, item, filepath)
      if not files or tree:IsDirectory(item) then return end
      addremove[relpath(ide:GetProject(), filepath)] = true
    end,
    onFiletreeFileRemove = function(self, tree, item, filepath)
      if not files or tree:IsDirectory(item) then return end
      addremove[relpath(ide:GetProject(), filepath)] = false
    end,
    onFiletreeFileRefresh = function(self)
      if not files then return end
 
      -- to save time only keep the file cache up-to-date if it's used
      if ide.config.commandbar.filecache then
        for key, val in ipairs(files or {}) do
          local ar = addremove[val]
          -- removed file, purge it from cache
          if ar == false then table.remove(files, key) end
          -- remove from the add-remove list, so that only non-existing files are left
          if ar ~= nil then addremove[val] = nil end
        end
        -- go over non-existing files and add them to the cache
        for key in pairs(addremove) do table.insert(files, key) end
      end
      addremove = {}
    end,
  })