fy36
2025-06-24 a3f50e5bd0d5ecbd0c5992da042fe4292dbb6911
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
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
-- Copyright 2011-18 Paul Kulchenko, ZeroBrane LLC
-- authors: Luxinia Dev (Eike Decker & Christoph Kubisch)
---------------------------------------------------------
 
local ide = ide
 
ide.filetree = {
  projdir = "",
  projdirlist = {},
  projdirpartmap = {},
  projtreeCtrl = nil,
  imglist = ide:CreateImageList("PROJECT",
    "FOLDER", "FOLDER-MAPPED", "FILE-NORMAL", "FILE-NORMAL-START",
    -- "file known" needs to be the last one as dynamic types can be added after it
    "FILE-KNOWN"),
  settings = {extensionignore = {}, startfile = {}, mapped = {}},
  extmap = {},
}
local filetree = ide.filetree
local iscaseinsensitive = wx.wxFileName("A"):SameAs(wx.wxFileName("a"))
local pathsep = GetPathSeparator()
local q = EscapeMagic
local image = { -- the order of ids has to match the order in the ImageList
  DIRECTORY = 0, DIRECTORYMAPPED = 1, FILEOTHER = 2, FILEOTHERSTART = 3,
  FILEKNOWN = 4,
}
 
local function getIcon(name, isdir)
  local project = ide:GetProject()
  local startfile = GetFullPathIfExists(project, filetree.settings.startfile[project])
  local ext = GetFileExt(name)
  local extmap = ide.filetree.extmap
  local known = extmap[ext] or ide:FindSpec(ext)
  if known and not extmap[ext] then
    local iconmap = ide.config.filetree.iconmap
    extmap[ext] = iconmap and ide.filetree.imglist:Add(ide:CreateFileIcon(ext)) or image.FILEKNOWN
  end
  local icon = isdir and image.DIRECTORY or known and extmap[ext] or image.FILEOTHER
  if startfile and startfile == name then icon = image.FILEOTHERSTART end
  return icon
end
 
local function treeAddDir(tree,parent_id,rootdir)
  local items = {}
  local item, cookie = tree:GetFirstChild(parent_id)
  while item:IsOk() do
    items[tree:GetItemText(item) .. tree:GetItemImage(item)] = item
    item, cookie = tree:GetNextChild(parent_id, cookie)
  end
 
  local cache = {}
  local curr
  local files = ide:GetFileList(rootdir)
  local dirmapped = {}
  local projpath = ide:GetProject()
  local ishoisted = tree:IsDirHoisted(parent_id)
  -- if this is a root, but not hoisted folder
  if tree:IsRoot(parent_id) and not ishoisted then
    local mapped = filetree.settings.mapped[projpath] or {}
    table.sort(mapped)
    -- insert into files at the sorted order
    for i, v in ipairs(mapped) do
      table.insert(files, i, v)
      dirmapped[v] = true
    end
  end
 
  for _, file in ipairs(files) do
    local name, dir = file:match("([^"..pathsep.."]+)("..pathsep.."?)$")
    local isdir = #dir>0
    if isdir or not filetree.settings.extensionignore[GetFileExt(name)] then
      local icon = getIcon(file, isdir)
 
      -- keep full name for the mapped directories
      if dirmapped[file] then
        name = file:gsub(q(projpath), ""):gsub(pathsep.."$","")
        icon = image.DIRECTORYMAPPED
      end
 
      local item = items[name .. icon]
      if item then -- existing item
        -- keep deleting items until we find item
        while true do
          local nextitem = (curr
            and tree:GetNextSibling(curr)
            or tree:GetFirstChild(parent_id))
          if not nextitem:IsOk() or name == tree:GetItemText(nextitem) then
            curr = nextitem
            break
          end
          PackageEventHandle("onFiletreeFileRemove", tree, nextitem, tree:GetItemFullName(nextitem))
          tree:Delete(nextitem)
        end
      else -- new item
        curr = (curr
          and tree:InsertItem(parent_id, curr, name, icon)
          or tree:PrependItem(parent_id, name, icon))
        if isdir then tree:SetItemHasChildren(curr, FileDirHasContent(file)) end
        PackageEventHandle("onFiletreeFileAdd", tree, curr, file)
      end
      if curr:IsOk() then cache[iscaseinsensitive and name:lower() or name] = curr end
    end
  end
 
  -- delete any leftovers (something that exists in the tree, but not on disk)
  while true do
    local nextitem = (curr
      and tree:GetNextSibling(curr)
      or tree:GetFirstChild(parent_id))
    if not nextitem:IsOk() then break end
    PackageEventHandle("onFiletreeFileRemove", tree, nextitem, tree:GetItemFullName(nextitem))
    tree:Delete(nextitem)
  end
 
  -- cache the mapping from names to tree items
  if ide.wxver >= "2.9.5" then
    local data = wx.wxLuaTreeItemData()
    data:SetData(cache)
    tree:SetItemData(parent_id, data)
  end
 
  tree:SetItemHasChildren(parent_id,
    tree:GetChildrenCount(parent_id, false) > 0)
 
  PackageEventHandle("onFiletreeFileRefresh", tree, parent_id, tree:GetItemFullName(parent_id))
end
 
local function treeSetRoot(tree,rootdir)
  if not ide:IsValidCtrl(tree) then return end
  if not wx.wxDirExists(rootdir) then return end
  tree:DeleteAllItems()
 
  local root_id = tree:AddRoot(rootdir, image.DIRECTORY)
  tree:SetItemHasChildren(root_id, true) -- make sure that the item can expand
  tree:Expand(root_id) -- this will also populate the tree
 
  -- sync with the current editor window and activate selected file
  local editor = ide:GetEditor()
  if editor then FileTreeMarkSelected(ide:GetDocument(editor):GetFilePath()) end
end
 
local function findItem(tree, match)
  local node = tree:GetRootItem()
  local label = tree:GetItemText(node)
 
  local s, e
  if iscaseinsensitive then
    s, e = string.find(match:lower(), label:lower(), 1, true)
  else
    s, e = string.find(match, label, 1, true)
  end
  if not s or s ~= 1 then return end
 
  for token in string.gmatch(string.sub(match,e+1), "[^%"..pathsep.."]+") do
    local data = tree:GetItemData(node)
    local cache = data and data:GetData()
    if cache and cache[iscaseinsensitive and token:lower() or token] then
      node = cache[iscaseinsensitive and token:lower() or token]
    else
      -- token is missing; may need to re-scan the folder; maybe new file
      local dir = tree:GetItemFullName(node)
      treeAddDir(tree,node,dir)
 
      local item, cookie = tree:GetFirstChild(node)
      while true do
        if not item:IsOk() then return end -- not found
        if tree:GetItemText(item) == token then
          node = item
          break
        end
        item, cookie = tree:GetNextChild(node, cookie)
      end
    end
  end
 
  -- this loop exits only when a match is found
  return node
end
 
local function treeSetConnectorsAndIcons(tree)
  tree:AssignImageList(filetree.imglist)
 
  local function isIt(item, imgtype) return tree:GetItemImage(item) == imgtype end
 
  function tree:IsDirectory(item_id) return isIt(item_id, image.DIRECTORY) end
  function tree:IsDirMapped(item_id) return isIt(item_id, image.DIRECTORYMAPPED) end
  function tree:IsDirHoisted(item_id)
    return (self:IsRoot(item_id) and
      not ide:IsSameDirectoryPath(ide:GetProject(), self:GetItemFullName(item_id)))
  end
  -- "file known" is a special case as it includes file types registered dynamically
  function tree:IsFileKnown(item_id) return tree:GetItemImage(item_id) >= image.FILEKNOWN end
  function tree:IsFileOther(item_id) return isIt(item_id, image.FILEOTHER) end
  function tree:IsFileStart(item_id) return isIt(item_id, image.FILEOTHERSTART) end
  function tree:IsRoot(item_id) return not tree:GetItemParent(item_id):IsOk() end
 
  function tree:GetFileImage(name) return getIcon(name, false) end
 
  local function saveSettings()
    ide:GetPackage('core.filetree'):SetSettings(filetree.settings)
  end
 
  function tree:FindItem(match)
    return findItem(self, (wx.wxIsAbsolutePath(match) or match == '') and match
      or MergeFullPath(ide:GetProject(), match))
  end
 
  function tree:GetItemFullName(item_id)
    local tree = self
    local str = tree:GetItemText(item_id)
    local cur = str
 
    while (#cur > 0) do
      item_id = tree:GetItemParent(item_id)
      if not item_id:IsOk() then break end
      cur = tree:GetItemText(item_id)
      if cur and #cur > 0 then str = MergeFullPath(cur, str) end
    end
    -- as root may already include path separator, normalize the path
    local fullPath = wx.wxFileName(str)
    fullPath:Normalize()
    return fullPath:GetFullPath()
  end
 
  function tree:RefreshChildren(node)
    node = node or tree:GetRootItem()
    treeAddDir(tree,node,tree:GetItemFullName(node))
    local item, cookie = tree:GetFirstChild(node)
    while true do
      if not item:IsOk() then return end
      if tree:IsExpanded(item) then tree:RefreshChildren(item) end
      item, cookie = tree:GetNextChild(node, cookie)
    end
  end
 
  local function refreshAncestors(node)
    -- when this method is called from END_EDIT, it causes infinite loop
    -- on OSX (wxwidgets 2.9.5) as Delete in treeAddDir calls END_EDIT again.
    -- disable handlers while the tree is populated and then enable back.
    tree:SetEvtHandlerEnabled(false)
    while node:IsOk() do
      local dir = tree:GetItemFullName(node)
      treeAddDir(tree,node,dir)
      node = tree:GetItemParent(node)
    end
    tree:SetEvtHandlerEnabled(true)
  end
 
  function tree:ActivateItem(item_id)
    local name = tree:GetItemFullName(item_id)
 
    local event = wx.wxTreeEvent(wx.wxEVT_COMMAND_TREE_ITEM_ACTIVATED, item_id:GetValue())
    if PackageEventHandle("onFiletreeActivate", tree, event, item_id) == false then
      return
    end
 
    -- refresh the folder
    if (tree:IsDirectory(item_id) or tree:IsDirMapped(item_id)) then
      if wx.wxDirExists(name) then
        treeAddDir(tree,item_id,name)
        tree:Toggle(item_id)
      else refreshAncestors(tree:GetItemParent(item_id)) end -- stale content
    else -- open file
      -- delay activation to keep the focus on the editor,
      -- as doubleclick sends it back to the tree on Windows.
      if wx.wxFileExists(name) then ide:DoWhenIdle(function() LoadFile(name,nil,true) end)
      else refreshAncestors(tree:GetItemParent(item_id)) end -- stale content
    end
  end
 
  function tree:UnmapDirectory(path)
    local project = ide:GetProject()
    if not project then return nil, "Project is not set" end
 
    local dir = wx.wxFileName.DirName(ide:MergePath(project, path))
    local mapped = filetree.settings.mapped[project] or {}
    for k = #mapped,1,-1 do
      if dir:SameAs(wx.wxFileName.DirName(mapped[k])) then table.remove(mapped, k) end
    end
    filetree.settings.mapped[project] = #mapped > 0 and mapped or nil
    saveSettings()
    refreshAncestors(self:GetRootItem())
    return true
  end
  function tree:MapDirectory(path)
    local project = ide:GetProject()
    if not project then return nil, "Project is not set" end
 
    if not path then
      local dirPicker = wx.wxDirDialog(ide.frame, TR("Choose a directory to map"),
        project ~= "" and project or wx.wxGetCwd(), wx.wxDIRP_DIR_MUST_EXIST)
      if dirPicker:ShowModal(true) ~= wx.wxID_OK then return end
      local dir = wx.wxFileName.DirName(FixDir(dirPicker:GetPath()))
      -- don't remap the project directory
      if dir:SameAs(wx.wxFileName.DirName(project)) then return end
      path = dir:GetFullPath()
    end
 
    local dir = wx.wxFileName.DirName(ide:MergePath(project, path))
    if not dir:DirExists() then return nil, "Directory doesn't exist" end
 
    local mapped = filetree.settings.mapped[project] or {}
    for _, m in ipairs(mapped) do
      if dir:SameAs(wx.wxFileName.DirName(m)) then return end -- already on the list
    end
    -- add to the list; the path includes trailing separator used for directory detection
    table.insert(mapped, dir:GetFullPath())
    filetree.settings.mapped[project] = mapped
    saveSettings()
    refreshAncestors(self:GetRootItem())
    return true
  end
 
  local empty = ""
  local function renameItem(itemsrc, target)
    local isdir = tree:IsDirectory(itemsrc)
    local isnew = tree:GetItemText(itemsrc) == empty
    local source = tree:GetItemFullName(itemsrc)
    local fn = wx.wxFileName(target)
 
    -- check if the target is the same as the source;
    -- SameAs check is not used here as "Test" and "test" are the same
    -- on case insensitive systems, but need to be allowed in renaming.
    if source == target then return end
 
    -- if target is a file, is already loaded and modified, then reject renaming
    local targetdocs = isnew and {} or (isdir
      and ide:FindDocumentsByPartialPath(target)
      or {ide:FindDocument(target)})
    for _, doc in ipairs(targetdocs) do
      if doc and doc:IsModified() then
        ide:ReportError(TR("Can't overwrite unsaved file '%s'."):format(doc:GetFilePath()))
        return false
      end
    end
 
    if PackageEventHandle("onFiletreeFilePreRename", tree, itemsrc, source, target) == false then
      return false
    end
 
    -- check if existing file/dir is going to be overwritten
    local overwrite = ((wx.wxFileExists(target) or wx.wxDirExists(target))
      and not wx.wxFileName(source):SameAs(fn))
    local doc = ide:FindDocument(target)
    if overwrite and doc then doc:SetActive() end
    if overwrite and not ApproveFileOverwrite() then return false end
 
    if not fn:Mkdir(tonumber("755",8), wx.wxPATH_MKDIR_FULL) then
      ide:ReportError(TR("Unable to create directory '%s'."):format(target))
      return false
    end
 
    if isnew then -- new directory or file; create manually
      if (isdir and not wx.wxFileName.DirName(target):Mkdir(tonumber("755",8), wx.wxPATH_MKDIR_FULL))
      or (not isdir and not FileWrite(target, "")) then
        ide:ReportError(TR("Unable to create file '%s'."):format(target))
        return false
      end
    else -- existing directory or file; rename/move it
      local ok, err = FileRename(source, target)
      if not ok then
        ide:ReportError(TR("Unable to rename file '%s'."):format(source)
          .."\nError: "..err)
        return false
      end
    end
 
    local expanded = tree:IsExpanded(itemsrc)
    local pos = tree:GetScrollPos(wx.wxVERTICAL)
 
    tree:Freeze()
 
    refreshAncestors(tree:GetItemParent(itemsrc))
 
    -- if not new, check if source is already opened in the editor
    local sourcedocs = isnew and {} or (isdir
      and ide:FindDocumentsByPartialPath(source)
      or {ide:FindDocument(source)})
    local targetdoc = ide:FindDocument(target)
 
    for _, doc in ipairs(sourcedocs) do
      local fullpath = doc:GetFilePath()
      -- when moving folders, /foo/bar/file.lua can be replaced with
      -- /foo/baz/bar/file.lua, so change /foo/bar to /foo/baz/bar
      local path = (not iscaseinsensitive and fullpath:gsub(q(source), target)
        or fullpath:lower():gsub(q(source:lower()), target))
 
      doc:SetFilePath(path)
      doc:SetFileName(wx.wxFileName(path):GetFullName())
      doc:SetFileModifiedTime(GetFileModTime(path))
      doc:SetTabText(doc:GetFileName())
      if doc:IsActive() then doc:SetActive() end
      -- reset target document if it matches one of the source docs
      -- this may happen if one of the opened documents is renamed
      if doc == targetdoc then targetdoc = nil end
    end
 
    -- close the target document, since the source has already been updated for it
    if targetdoc and #sourcedocs > 0 then targetdoc:Close() end
 
    local itemdst = tree:FindItem(target)
    if itemdst then
      tree:UnselectAll()
      tree:RefreshChildren(tree:GetItemParent(itemdst))
      tree:SetFocusedItem(itemdst)
      tree:SelectItem(itemdst)
      if expanded then tree:Expand(itemdst) end
      tree:SetScrollPos(wx.wxVERTICAL, pos)
    end
 
    tree:Thaw()
 
    PackageEventHandle("onFiletreeFileRename", tree, itemsrc, source, target)
 
    return true
  end
  local function deleteItem(item_id)
    -- if delete is for mapped directory, unmap it instead
    if tree:IsDirMapped(item_id) then
      tree:UnmapDirectory(tree:GetItemText(item_id))
      return
    end
 
    local isdir = tree:IsDirectory(item_id)
    local source = tree:GetItemFullName(item_id)
 
    if PackageEventHandle("onFiletreeFilePreDelete", tree, item_id, source) == false then
      return false
    end
 
    if isdir and FileDirHasContent(source..pathsep) then return false end
    if wx.wxMessageBox(
      TR("Do you want to delete '%s'?"):format(source),
      ide:GetProperty("editormessage"),
      wx.wxYES_NO + wx.wxCENTRE, ide.frame) ~= wx.wxYES then return false end
 
    if isdir then
      if not wx.wxRmdir(source) then
        ide:ReportError(TR("Unable to delete directory '%s': %s")
          :format(source, wx.wxSysErrorMsg()))
      end
    else
      local doc = ide:FindDocument(source)
      if doc then doc:Close() end
      if not wx.wxRemoveFile(source) then
        ide:ReportError(TR("Unable to delete file '%s': %s")
          :format(source, wx.wxSysErrorMsg()))
      end
    end
    refreshAncestors(tree:GetItemParent(item_id))
    PackageEventHandle("onFiletreeFileDelete", tree, item_id, source)
    return true
  end
 
  if wx.wxLuaFileDropTarget then
    -- localizing FileDropTarget doesn't work in wxlua 2.8.13, so keep it in a field
    ide.filetree.filedrop = wx.wxLuaFileDropTarget()
    ide.filetree.filedrop.OnDropFiles = function(self, x, y, filenames)
      local item_id = tree:HitTest(wx.wxPoint(x,y))
      -- set project if one file moved over the project directory
      if item_id:IsOk() and not tree:GetItemParent(item_id):IsOk() and #filenames == 1 then
        ide:SetProject(filenames[1])
      else
        for i = 1, #filenames do tree:MapDirectory(filenames[i]) end
      end
      return true
    end
    tree:SetDropTarget(ide.filetree.filedrop)
  end
 
  tree:Connect(wx.wxEVT_COMMAND_TREE_ITEM_COLLAPSED,
    function (event)
      PackageEventHandle("onFiletreeCollapse", tree, event, event:GetItem())
    end)
  tree:Connect(wx.wxEVT_COMMAND_TREE_ITEM_EXPANDED,
    function (event)
      PackageEventHandle("onFiletreeExpand", tree, event, event:GetItem())
    end)
  tree:Connect(wx.wxEVT_COMMAND_TREE_ITEM_COLLAPSING,
    function (event)
      if PackageEventHandle("onFiletreePreCollapse", tree, event, event:GetItem()) == false then
        return
      end
    end)
  tree:Connect(wx.wxEVT_COMMAND_TREE_ITEM_EXPANDING,
    function (event)
      local item_id = event:GetItem()
      if PackageEventHandle("onFiletreePreExpand", tree, event, item_id) == false then
        return
      end
 
      local dir = tree:GetItemFullName(item_id)
      if wx.wxDirExists(dir) then treeAddDir(tree,item_id,dir) -- refresh folder
      else refreshAncestors(tree:GetItemParent(item_id)) end -- stale content
      return true
    end)
  tree:Connect(wx.wxEVT_COMMAND_TREE_ITEM_ACTIVATED,
    function (event)
      tree:ActivateItem(event:GetItem())
    end)
 
  -- refresh the tree
  local function refreshChildren()
    tree:RefreshChildren()
    -- now mark the current file (if it was previously disabled)
    local editor = ide:GetEditor()
    if editor then FileTreeMarkSelected(ide:GetDocument(editor):GetFilePath()) end
  end
 
  -- handle context menu
  local function addItem(item_id, name, img)
    local isdir = tree:IsDirectory(item_id)
    local ismapped = tree:IsDirMapped(item_id)
    local parent = (isdir or ismapped) and item_id or tree:GetItemParent(item_id)
    if isdir or ismapped then tree:Expand(item_id) end -- expand to populate if needed
 
    local item = tree:PrependItem(parent, name, img)
    tree:SetItemHasChildren(parent, true)
    -- temporarily disable expand as we don't need this node populated
    if not tree:IsVisible(item) then
      tree:SetEvtHandlerEnabled(false)
      tree:EnsureVisible(item)
      tree:SetEvtHandlerEnabled(true)
    end
    return item
  end
 
  local function updateItemImage(item_id)
    if item_id and item_id:IsOk() then
      local path = tree:GetItemFullName(item_id)
      tree:SetItemImage(item_id, getIcon(path))
      -- if the file is opened, then change the tab icon as well
      local doc = ide:FindDocument(path)
      if doc then doc:SetTabText() end
    end
  end
 
  local function unsetStartFile()
    local project = ide:GetProject()
    if not project then return end
 
    local startfile = filetree.settings.startfile[project]
    filetree.settings.startfile[project] = nil
    if startfile then
      updateItemImage(tree:FindItem(startfile))
    end
    return startfile
  end
 
  local function setStartFile(item_id)
    local project = ide:GetProject()
    if not project then return end
 
    local startfile = tree:GetItemFullName(item_id):gsub(q(project), "")
    filetree.settings.startfile[project] = startfile
    updateItemImage(item_id)
    return startfile
  end
 
  function tree:GetStartFile()
    local project = ide:GetProject()
    return project and filetree.settings.startfile[project]
  end
 
  function tree:SetStartFile(path)
    local item_id
    local project = ide:GetProject()
    if project and type(path) == "string" then
      local startfile = path:gsub(q(project), "")
      item_id = self:FindItem(startfile)
    end
    -- unset if explicitly requested or the replacement has been found
    if not path or item_id then unsetStartFile() end
    if item_id then setStartFile(item_id) end
 
    saveSettings()
    return item_id
  end
 
  tree:Connect(ID.NEWFILE, wx.wxEVT_COMMAND_MENU_SELECTED,
    function()
      tree:EditLabel(addItem(tree:GetFocusedItem(), empty, image.FILEOTHER))
    end)
  tree:Connect(ID.NEWDIRECTORY, wx.wxEVT_COMMAND_MENU_SELECTED,
    function()
      tree:EditLabel(addItem(tree:GetFocusedItem(), empty, image.DIRECTORY))
    end)
  tree:Connect(ID.RENAMEFILE, wx.wxEVT_COMMAND_MENU_SELECTED,
    function() tree:EditLabel(tree:GetFocusedItem()) end)
  tree:Connect(ID.DELETEFILE, wx.wxEVT_COMMAND_MENU_SELECTED,
    function() deleteItem(tree:GetFocusedItem()) end)
  tree:Connect(ID.COPYFULLPATH, wx.wxEVT_COMMAND_MENU_SELECTED,
    function() ide:CopyToClipboard(tree:GetItemFullName(tree:GetFocusedItem())) end)
  tree:Connect(ID.OPENEXTENSION, wx.wxEVT_COMMAND_MENU_SELECTED,
    function()
      local fname = tree:GetItemFullName(tree:GetFocusedItem())
      local ext = '.'..wx.wxFileName(fname):GetExt()
      local ft = wx.wxTheMimeTypesManager:GetFileTypeFromExtension(ext)
      if ft then
        local cmd = ft:GetOpenCommand(fname:gsub('"','\\"'))
        -- some programs on Windows, when started by rundll32.exe (for example, PhotoViewer)
        -- accept files with spaces in names ONLY if they are not in quotes.
        if ide.osname == "Windows" and cmd:find("rundll32%.exe") then
          cmd = ft:GetOpenCommand(""):gsub('""%s*$', '')..fname
        end
        wx.wxExecute(cmd, wx.wxEXEC_ASYNC)
      end
    end)
  tree:Connect(ID.REFRESH, wx.wxEVT_COMMAND_MENU_SELECTED,
    function() refreshChildren() end)
  tree:Connect(ID.SHOWLOCATION, wx.wxEVT_COMMAND_MENU_SELECTED,
    function() ShowLocation(tree:GetItemFullName(tree:GetFocusedItem())) end)
  tree:Connect(ID.HIDEEXTENSION, wx.wxEVT_COMMAND_MENU_SELECTED,
    function()
      local ext = GetFileExt(tree:GetItemText(tree:GetFocusedItem()))
      filetree.settings.extensionignore[ext] = true
      saveSettings()
      refreshChildren()
    end)
  tree:Connect(ID.SHOWEXTENSIONALL, wx.wxEVT_COMMAND_MENU_SELECTED,
    function()
      filetree.settings.extensionignore = {}
      saveSettings()
      refreshChildren()
    end)
  tree:Connect(ID.SETSTARTFILE, wx.wxEVT_COMMAND_MENU_SELECTED,
    function()
      tree:SetStartFile(tree:GetItemFullName(tree:GetFocusedItem()))
    end)
  tree:Connect(ID.UNSETSTARTFILE, wx.wxEVT_COMMAND_MENU_SELECTED,
    function()
      tree:SetStartFile()
    end)
  tree:Connect(ID.MAPDIRECTORY, wx.wxEVT_COMMAND_MENU_SELECTED,
    function()
      tree:MapDirectory()
    end)
  tree:Connect(ID.UNMAPDIRECTORY, wx.wxEVT_COMMAND_MENU_SELECTED,
    function()
      tree:UnmapDirectory(tree:GetItemText(tree:GetFocusedItem()))
    end)
  tree:Connect(ID.HOISTDIRECTORY, wx.wxEVT_COMMAND_MENU_SELECTED,
    function()
      treeSetRoot(tree, tree:GetItemFullName(tree:GetFocusedItem()))
    end)
  tree:Connect(ID.UNHOISTDIRECTORY, wx.wxEVT_COMMAND_MENU_SELECTED,
    function()
      treeSetRoot(tree, ide:GetProject())
    end)
  tree:Connect(ID.PROJECTDIRFROMDIR, wx.wxEVT_COMMAND_MENU_SELECTED,
    function()
      ide:SetProject(tree:GetItemFullName(tree:GetFocusedItem()))
    end)
 
  tree:Connect(wx.wxEVT_COMMAND_TREE_ITEM_MENU,
    function (event)
      local item_id = event:GetItem()
      tree:SetFocusedItem(item_id)
 
      local renamelabel = (tree:IsRoot(item_id)
        and TR("&Edit Project Directory")
        or TR("&Rename"))
      local fname = tree:GetItemText(item_id)
      local ext = GetFileExt(fname)
      local project = ide:GetProject()
      local startfile = project and filetree.settings.startfile[project]
      local menu = ide:MakeMenu {
        { ID.NEWFILE, TR("New &File") },
        { ID.NEWDIRECTORY, TR("&New Directory") },
        { },
        { ID.RENAMEFILE, renamelabel..KSC(ID.RENAMEFILE) },
        { ID.DELETEFILE, TR("&Delete")..KSC(ID.DELETEFILE) },
        { ID.REFRESH, TR("Refresh") },
        { },
        { ID.HIDEEXTENSION, TR("Hide '.%s' Files"):format(ext) },
        { },
        { ID.SETSTARTFILE, TR("Set As Start File") },
        { ID.UNSETSTARTFILE, TR("Unset '%s' As Start File"):format(startfile or "<none>") },
        { },
        { ID.HOISTDIRECTORY, TR("Hoist Directory") },
        { ID.UNHOISTDIRECTORY, TR("Unhoist Directory") },
        { ID.MAPDIRECTORY, TR("Map Directory...") },
        { ID.UNMAPDIRECTORY, TR("Unmap Directory") },
        { ID.OPENEXTENSION, TR("Open With Default Program") },
        { ID.COPYFULLPATH, TR("Copy Full Path") },
        { ID.SHOWLOCATION, TR("Open Containing Folder") },
      }
      local extlist = {
        {},
        { ID.SHOWEXTENSIONALL, TR("Show All Files"), TR("Show all files") },
      }
      for extignore in pairs(filetree.settings.extensionignore) do
        local id = ID("filetree.showextension."..extignore)
        table.insert(extlist, 1, {id, '.'..extignore})
        menu:Connect(id, wx.wxEVT_COMMAND_MENU_SELECTED, function()
          filetree.settings.extensionignore[extignore] = nil
          saveSettings()
          refreshChildren()
        end)
      end
      local _, _, hideextpos = ide:FindMenuItem(ID.HIDEEXTENSION, menu)
      assert(hideextpos, "Can't find HideExtension menu item")
      menu:Insert(hideextpos+1, wx.wxMenuItem(menu, ID.SHOWEXTENSION,
        TR("Show Hidden Files"), TR("Show files previously hidden"),
        wx.wxITEM_NORMAL, ide:MakeMenu(extlist)))
 
      local projectdirectorymenu = ide:MakeMenu {
        { },
        {ID.PROJECTDIRCHOOSE, TR("Choose...")..KSC(ID.PROJECTDIRCHOOSE), TR("Choose a project directory")},
        {ID.PROJECTDIRFROMDIR, TR("Set To Selected Directory")..KSC(ID.PROJECTDIRFROMDIR), TR("Set project directory to the selected one")},
      }
      local projectdirectory = wx.wxMenuItem(menu, ID.PROJECTDIR,
        TR("Project Directory"), TR("Set the project directory to be used"),
        wx.wxITEM_NORMAL, projectdirectorymenu)
      local _, _, unmapdirpos = ide:FindMenuItem(ID.UNMAPDIRECTORY, menu)
      assert(unmapdirpos, "Can't find UnMapDirectory menu item")
      menu:Insert(unmapdirpos+1, projectdirectory)
      FileTreeProjectListUpdate(projectdirectorymenu, 0)
 
      -- disable Delete on non-empty directories
      local isdir = tree:IsDirectory(item_id)
      local ismapped = tree:IsDirMapped(item_id)
      menu:Destroy(ismapped and ID.MAPDIRECTORY or ID.UNMAPDIRECTORY)
 
      local isroot = tree:IsRoot(item_id)
      local ishoisted = tree:IsDirHoisted(tree:GetRootItem())
      menu:Enable(ID.UNHOISTDIRECTORY, ishoisted)
      menu:Enable(ID.HOISTDIRECTORY, isdir and not isroot or ismapped)
      if not ishoisted then menu:Destroy(ID.UNHOISTDIRECTORY) end
      if ishoisted and (not isdir or isroot) then menu:Destroy(ID.HOISTDIRECTORY) end
 
      if not startfile then menu:Destroy(ID.UNSETSTARTFILE) end
      if ismapped then menu:Enable(ID.RENAMEFILE, false) end
      if isdir then
        local source = tree:GetItemFullName(item_id)
        menu:Enable(ID.DELETEFILE, not FileDirHasContent(source..pathsep))
        menu:Enable(ID.OPENEXTENSION, false)
        menu:Enable(ID.HIDEEXTENSION, false)
      else
        local ft = wx.wxTheMimeTypesManager:GetFileTypeFromExtension('.'..ext)
        menu:Enable(ID.OPENEXTENSION, ft and #ft:GetOpenCommand("") > 0)
        menu:Enable(ID.HIDEEXTENSION, not filetree.settings.extensionignore[ext])
        menu:Enable(ID.PROJECTDIRFROMDIR, false)
      end
      menu:Enable(ID.SETSTARTFILE, tree:IsFileOther(item_id) or tree:IsFileKnown(item_id))
      menu:Enable(ID.SHOWEXTENSION, next(filetree.settings.extensionignore) ~= nil)
 
      PackageEventHandle("onMenuFiletree", menu, tree, event)
 
      -- stopping/restarting garbage collection is generally not needed,
      -- but on Linux not stopping is causing crashes (wxwidgets 2.9.5 and 3.1.0)
      -- when symbol indexing is done while popup menu is open (with gc methods in the trace).
      -- this only happens when EVT_IDLE is called when popup menu is open.
      collectgarbage("stop")
 
      -- stopping UI updates is generally not needed as well,
      -- but it's causing a crash on OSX (wxwidgets 2.9.5 and 3.1.0)
      -- when symbol indexing is done while popup menu is open, so it's disabled
      local interval = wx.wxUpdateUIEvent.GetUpdateInterval()
      wx.wxUpdateUIEvent.SetUpdateInterval(-1) -- don't update
 
      tree:PopupMenu(menu)
      wx.wxUpdateUIEvent.SetUpdateInterval(interval)
      collectgarbage("restart")
    end)
 
  tree:Connect(wx.wxEVT_RIGHT_DOWN,
    function (event)
      local item_id = tree:HitTest(event:GetPosition())
      if PackageEventHandle("onFiletreeRDown", tree, event, item_id and item_id:IsOk() and item_id or nil) == false then
        return
      end
      event:Skip()
    end)
 
  -- toggle a folder on a single click
  tree:Connect(wx.wxEVT_LEFT_DOWN,
    function (event)
      -- only toggle if this is a folder and the click is on the item line
      -- (exclude the label as it's used for renaming and dragging)
      local mask = (wx.wxTREE_HITTEST_ONITEMINDENT
        + wx.wxTREE_HITTEST_ONITEMICON + wx.wxTREE_HITTEST_ONITEMRIGHT)
      local item_id, flags = tree:HitTest(event:GetPosition())
 
      if PackageEventHandle("onFiletreeLDown", tree, event, item_id and item_id:IsOk() and item_id or nil) == false then
        return
      end
 
      if item_id and item_id:IsOk() and bit.band(flags, mask) > 0 then
        if tree:IsDirectory(item_id) then
          tree:Toggle(item_id)
        else
          local name = tree:GetItemFullName(item_id)
          if wx.wxFileExists(name) then LoadFile(name,nil,true) end
        end
      else
        event:Skip()
      end
      return true
    end)
  local parent
  tree:Connect(wx.wxEVT_COMMAND_TREE_BEGIN_LABEL_EDIT,
    function (event)
      local itemsrc = event:GetItem()
      parent = tree:GetItemParent(itemsrc)
      if not itemsrc:IsOk() or tree:IsDirMapped(itemsrc) then event:Veto() end
    end)
  tree:Connect(wx.wxEVT_COMMAND_TREE_END_LABEL_EDIT,
    function (event)
      -- veto the event to keep the original label intact as the tree
      -- is going to be refreshed with the correct names.
      event:Veto()
 
      local itemsrc = event:GetItem()
      if not itemsrc:IsOk() then return end
 
      local label = event:GetLabel():gsub("^%s+$","") -- clean all spaces
 
      -- edited the root element; set the new project directory if needed
      local cancelled = event:IsEditCancelled()
      if tree:IsRoot(itemsrc) then
        if not cancelled and wx.wxDirExists(label) then
          ide:SetProject(label)
        end
        return
      end
 
      if not parent or not parent:IsOk() then return end
      local target = MergeFullPath(tree:GetItemFullName(parent), label)
      if cancelled or label == empty then refreshAncestors(parent)
      elseif target then
        -- wxwidgets v2.9.5 and earlier crashes when the IDE loses focus
        -- during renaming a file that has a conflict with an exising one
        -- (caused by the same END_LABEL_EDIT even triggered one more time).
        if ide.osname == "Linux" and ide.wxver <= "2.9.5" then return end
        if not renameItem(itemsrc, target) then refreshAncestors(parent) end
      end
    end)
 
  if ide.wxver >= "3.1.3" then
    -- provide workaround for white background in a focused item in the tree
    -- on Linux when the tree itself is unfocused (may be gtk3-only issue);
    -- enable on all platforms, as the brackground is low contrast on MacOS
    -- and blue-colored on Windows
    tree:Connect(wx.wxEVT_KILL_FOCUS, function(event) tree:ClearFocusedItem() end)
  end
 
  local itemsrc
  tree:Connect(wx.wxEVT_COMMAND_TREE_BEGIN_DRAG,
    function (event)
      if ide.config.filetree.mousemove and tree:GetItemParent(event:GetItem()):IsOk() then
        itemsrc = event:GetItem()
        event:Allow()
      end
    end)
  tree:Connect(wx.wxEVT_COMMAND_TREE_END_DRAG,
    function (event)
      local itemdst = event:GetItem()
      if not itemdst:IsOk() or not itemsrc:IsOk() then return end
 
      -- check if itemdst is a folder
      local target = tree:GetItemFullName(itemdst)
      if wx.wxDirExists(target) then
        local source = tree:GetItemFullName(itemsrc)
        -- check if moving the directory and target is a subfolder of source
        if (target..pathsep):find("^"..q(source)..pathsep) then return end
        renameItem(itemsrc, MergeFullPath(target, tree:GetItemText(itemsrc)))
      end
    end)
end
 
-- project
local projtree = ide:CreateTreeCtrl(ide.frame, wx.wxID_ANY,
  wx.wxDefaultPosition, wx.wxDefaultSize,
  wx.wxTR_HAS_BUTTONS + wx.wxTR_MULTIPLE + wx.wxTR_LINES_AT_ROOT
  + wx.wxTR_EDIT_LABELS + wx.wxNO_BORDER)
projtree:SetFont(ide.font.tree)
filetree.projtreeCtrl = projtree
 
ide:GetProjectNotebook():AddPage(projtree, TR("Project"), true)
 
-- proj connectors
-- ---------------
 
treeSetConnectorsAndIcons(projtree)
 
-- proj functions
-- ---------------
 
local function appendPathSep(dir)
  return (dir and #dir > 0 and wx.wxFileName.DirName(dir):GetFullPath() or nil)
end
 
function filetree:updateProjectDir(newdir)
  if (not newdir) or not wx.wxDirExists(newdir) then return nil, "Directory doesn't exist" end
  local dirname = wx.wxFileName.DirName(newdir)
 
  if filetree.projdir and #filetree.projdir > 0
  and dirname:SameAs(wx.wxFileName.DirName(filetree.projdir)) then return false end
 
  -- strip the last path separator if any
  local newdir = dirname:GetPath(wx.wxPATH_GET_VOLUME)
 
  -- save the current interpreter as it may be reset in onProjectClose
  -- when the project event handlers manipulates interpreters
  local intfname = ide.interpreter and ide.interpreter.fname
 
  if filetree.projdir and #filetree.projdir > 0 then
    PackageEventHandle("onProjectClose", appendPathSep(filetree.projdir))
  end
 
  PackageEventHandle("onProjectPreLoad", appendPathSep(newdir))
 
  if ide.config.projectautoopen and filetree.projdir then
    StoreRestoreProjectTabs(filetree.projdir, newdir, intfname)
  end
 
  filetree.projdir = newdir
  filetree.projdirpartmap = {}
 
  PrependStringToArray(
    filetree.projdirlist,
    newdir,
    ide.config.projecthistorylength,
    function(s1, s2) return dirname:SameAs(wx.wxFileName.DirName(s2)) end)
 
  ide:SetProject(newdir,true)
  treeSetRoot(projtree,newdir)
 
  -- refresh Recent Projects menu item
  ide.frame:AddPendingEvent(wx.wxUpdateUIEvent(ID.RECENTPROJECTS))
 
  PackageEventHandle("onProjectLoad", appendPathSep(newdir))
 
  return true
end
 
function FileTreeSetProjects(tab)
  filetree.projdirlist = tab
  if (tab and tab[1]) then
    filetree:updateProjectDir(tab[1])
  end
end
 
function FileTreeGetProjects() return filetree.projdirlist end
 
local function getProjectLabels()
  local labels = {}
  local fmt = ide.config.format.menurecentprojects or '%f'
  for _, proj in ipairs(FileTreeGetProjects()) do
    local config = ide.session.projects[proj]
    local intfname = config and config[2] and config[2].interpreter or ide.interpreter:GetFileName()
    local interpreter = intfname and ide.interpreters[intfname]
    local parts = wx.wxFileName(proj..pathsep):GetDirs()
    table.insert(labels, ide:ExpandPlaceholders(fmt, {
          f = proj,
          i = interpreter and interpreter:GetName() or (intfname or '')..'?',
          s = parts[#parts] or '',
        }))
  end
  return labels
end
 
function FileTreeProjectListClear()
  -- remove all items from the list except the current one
  filetree.projdirlist = {ide:GetProject()}
end
 
function FileTreeProjectListUpdate(menu, items)
  -- protect against recent project menu not being present
  if not ide:FindMenuItem(ID.RECENTPROJECTS) then return end
 
  local list = getProjectLabels()
  for i=#list, 1, -1 do
    local id = ID("file.recentprojects."..i)
    local label = list[i]
    if i <= items then -- this is an existing item; update the label
      menu:FindItem(id):SetItemLabel(label)
    else -- need to add an item
      local item = wx.wxMenuItem(menu, id, label, "")
      menu:Insert(items, item)
      ide.frame:Connect(id, wx.wxEVT_COMMAND_MENU_SELECTED, function()
          wx.wxSafeYield() -- let the menu on screen (if any) disappear
          ide:SetProject(FileTreeGetProjects()[i])
        end)
    end
    -- disable the currently selected project
    if i == 1 then menu:Enable(id, false) end
  end
  for i=items, #list+1, -1 do -- delete the rest if the list got shorter
    menu:Delete(menu:FindItemByPosition(i-1))
  end
  return #list
end
 
local curr_file
function FileTreeMarkSelected(file)
  if not file or not filetree.projdir or #filetree.projdir == 0
  or not ide:IsValidCtrl(projtree) then return end
 
  local item_id = wx.wxIsAbsolutePath(file) and projtree:FindItem(file)
 
  -- if the select item is different from the current one
  -- or the current one is the same, but not bold (which may happen when
  -- the project is changed to one that includes the current item)
  if curr_file ~= file
  or item_id and not projtree:IsBold(item_id) then
    if curr_file then
      local curr_id = wx.wxIsAbsolutePath(curr_file) and projtree:FindItem(curr_file)
      if curr_id and projtree:IsBold(curr_id) then
        projtree:SetItemBold(curr_id, false)
        PackageEventHandle("onFiletreeFileMarkSelected", projtree, curr_id, curr_file, false)
      end
    end
    if item_id then
      if not projtree:IsVisible(item_id) then
        projtree:EnsureVisible(item_id)
        -- it's supposed to be enough to do EnsureVisible,
        -- but occasionally it's positioned one item too high (wxwidgets 3.1.1 on Win),
        -- so scroll to make sure the item really is visible
        projtree:ScrollTo(item_id)
        projtree:SetScrollPos(wx.wxHORIZONTAL, 0, true)
        if ide.osname == "Windows" then
          -- the following is a workaround for SetScrollPos not scrolling in wxwidgets 3.1.3
          -- https://trac.wxwidgets.org/ticket/18543
          projtree:Freeze()
          projtree:Thaw()
        end
      end
      projtree:SetItemBold(item_id, true)
      PackageEventHandle("onFiletreeFileMarkSelected", projtree, item_id, file, true)
    end
    curr_file = file
    if ide.wxver < "2.9.5" and ide.osname == 'Macintosh' then
      projtree:Refresh()
    end
  end
end
 
function FileTreeFindByPartialName(name)
  -- check if it's already cached
  if filetree.projdirpartmap[name] then return filetree.projdirpartmap[name] end
 
  -- this function may get a partial name that starts with ... and has
  -- an abbreviated path (as generated by stack traces);
  -- remove starting "..." if any and escape
  local pattern = q(name:gsub("^%.%.%.","")):gsub("[\\/]", "[\\/]").."$"
  local lpattern = pattern:lower()
 
  for _, file in ipairs(ide:GetFileList(filetree.projdir, true)) do
    if file:find(pattern) or iscaseinsensitive and file:lower():find(lpattern) then
      filetree.projdirpartmap[name] = file
      return file
    end
  end
  return
end
 
local watchers, blacklist, watcher = {}, {}
local function watchDir(path)
  if watcher and not watchers[path] and not blacklist[path] then
    local _ = wx.wxLogNull() -- disable error reporting; will report as needed
    local ok  = watcher:Add(wx.wxFileName.DirName(path),
      wx.wxFSW_EVENT_CREATE + wx.wxFSW_EVENT_DELETE + wx.wxFSW_EVENT_RENAME)
    if not ok then
      blacklist[path] = true
      ide:GetOutput():Error(("Can't set watch for '%s': %s"):format(path, wx.wxSysErrorMsg()))
      return nil, wx.wxSysErrorMsg()
    end
  end
  -- keep track of watchers even if `watcher` is not yet set to set them later
  watchers[path] = watcher ~= nil
  return watchers[path]
end
local function unWatchDir(path)
  if watcher and watchers[path] then watcher:Remove(wx.wxFileName.DirName(path)) end
  watchers[path] = nil
end
 
local function syncTree(editor)
    local doc = editor and ide:GetDocument(editor)
    FileTreeMarkSelected(doc and doc:GetFilePath() or '')
    SetAutoRecoveryMark()
    ide:SetTitle()
end
 
local package = ide:AddPackage('core.filetree', {
    onProjectClose = function(plugin, project)
      if watcher then watcher:RemoveAll() end
      watchers = {}
    end,
 
    -- watcher can only be properly setup when MainLoop is already running, so use first idle event
    onIdleOnce = function(plugin)
      if not ide.config.filetree.showchanges or not wx.wxFileSystemWatcher then return end
      if not watcher then
        watcher = wx.wxFileSystemWatcher()
        watcher:SetOwner(ide:GetMainFrame())
 
        local needrefresh = {}
        ide:GetMainFrame():Connect(wx.wxEVT_FSWATCHER, function(event)
            -- using `GetNewPath` to make it work with rename operations
            needrefresh[event:GetNewPath():GetFullPath()] = event:GetChangeType()
            ide:DoWhenIdle(function()
                for file, kind in pairs(needrefresh) do
                  -- if the file is removed, try to find a non-existing file in the same folder
                  -- as this will trigger a refresh of that folder
                  local path = MergeFullPath(file, kind == wx.wxFSW_EVENT_DELETE and "../\1"  or "")
                  local tree = ide:GetProjectTree() -- project tree may be hidden/disabled
                  if ide:IsValidCtrl(tree) then tree:FindItem(path) end
                end
                needrefresh = {}
              end)
          end)
      end
      -- start watching cached paths
      for path, active in pairs(watchers) do
        if not active then watchDir(path) end
      end
    end,
 
    -- check on PreExpand when expanding (as the folder may not expand if it's empty)
    onFiletreePreExpand = function(plugin, tree, event, item_id)
      watchDir(tree:GetItemFullName(item_id))
    end,
 
    -- check on Collapse when collapsing to make sure it's unwatched only when collapsed
    onFiletreeCollapse = function(plugin, tree, event, item_id)
      -- only unwatch if the directory is not empty;
      -- otherwise it's collapsed without ability to expand
      if tree:GetChildrenCount(item_id, false) > 0 then unWatchDir(tree:GetItemFullName(item_id)) end
    end,
 
    onEditorFocusSet = function(plugin, editor)
      -- need to delay the sync, as the document may not yet exist for the editor
      ide:DoWhenIdle(function() if ide:IsValidCtrl(editor) then syncTree(editor) end end,
        "core.filetree.oneditorfocusset")
    end,
 
    onEditorClose = function(plugin, editor)
      -- check if the last document is being closed
      if #ide:GetDocumentList() <= 1 then syncTree() end
    end,
  })
MergeSettings(filetree.settings, package:GetSettings())