ef5c90085202443c692e37e7a512147a219bc5fd
[sdk] / ide / src / dialogs / FindInFilesDialog.ec
1 import "ide"
2 import "layout"
3
4 enum FindInFilesMode { directory, workspace, project };
5
6 class FindInFilesDialog : Window
7 {
8    text = $"Find In Files";
9    background = formColor;
10    borderStyle = sizable;
11    minClientSize = { 440, 208 };
12    maxClientSize = { 640, 208 };
13    hasClose = true;
14    tabCycle = true;
15    size = { 640, 208 };
16    autoCreate = false;
17
18 public:
19    property const char * searchString { set { findContent.contents = value; } get { return findContent.contents; } };
20    property bool contentWholeWord { set { contentWholeWord.checked = value; } get { return contentWholeWord.checked; } };
21    property bool contentMatchCase { set { contentMatchCase.checked = value; } get { return contentMatchCase.checked; } };
22    property const char * currentDirectory
23    {
24       set
25       {
26          GetWorkingDir(currentDirectory, MAX_DIRECTORY);
27          PathCat(currentDirectory, value);
28          FileFixCase(currentDirectory);
29          findWhere.path = currentDirectory;
30       }
31       get { return (char *)currentDirectory; }
32    };
33    property FileFilter * filters { set { filters = value; } get { return filters; } };
34    // Replace with Array system
35    property int sizeFilters
36    {
37       set
38       {
39          int numFilters = value / sizeof(FileFilter);
40          int c;
41
42          sizeFilters = value;
43
44          // File Extension Filter
45          filterDrop.Clear();
46          // filterDrop.AddField(null);
47          fileFilter = 0;
48          if(filters)
49          {
50             for(c = 0; c<numFilters; c++)
51             {
52                DataRow row = filterDrop.AddString(filters[c].name);
53                if(filters[c].extensions && !strcmp(filters[c].extensions, "ec, eh, c, cpp, cc, cxx, h, hpp, hh, hxx"))
54                   fileFilter = c;
55                row.tag = c;
56             }
57          }
58          if(!numFilters)
59             filterDrop.AddString($"All files");
60
61          //if(fileFilter >= numFilters)
62          //  fileFilter = 0;
63          filterDrop.currentRow = filterDrop.FindRow(fileFilter);
64       }
65       get { return sizeFilters; }
66    };
67    property int filter { set { fileFilter = value; } get { return fileFilter; } };
68
69    property FindInFilesMode mode
70    {
71       set
72       {
73          switch(value)
74          {
75             case directory:
76                findIn.SelectRow(inDirectoryRow);
77                break;
78             case workspace:
79                findIn.SelectRow(inWorkspaceRow);
80                break;
81             case project:
82             {
83                DataRow row;
84                for(row = findIn.firstRow; row; row = row.next)
85                   if(row != inDirectoryRow && row != inWorkspaceRow)
86                      break;
87                if(row)
88                   findIn.SelectRow(row);
89                break;
90             }
91          }
92          lastSelectionMode = value;
93       }
94       get
95       {
96          if(findIn.currentRow == inDirectoryRow)
97             return directory;
98          else if(findIn.currentRow == inWorkspaceRow)
99             return workspace;
100          else
101             return project;
102       }
103    }
104
105    property char * findDir
106    {
107       get { return searchThread.mode == directory ? (char *)searchThread.dir : null; }
108    }
109       /*if(searchThread.mode == directory)
110          ide.outputView.findDir = CopyString(searchThread.dir);
111       else if(searchThread.mode == project)
112          ide.outputView.findProject = searchThread.project;*/
113    property Project findProject
114    {
115       get { return searchThread.mode == project ? searchThread.project : null; }
116    }
117
118    property bool replaceMode
119    {
120       set
121       {
122          if(value != replaceMode)
123          {
124             int h = value ? 236 : 208;
125             find.text = value ? $"Replace" : $"Find";
126             find.hotKey = value ? altF : altR; // tocheck: the hotkey keeps getting lost
127             llreplaceWith.visible = value;
128             text = value ? $"Replace In Files" : $"Find In Files";
129             minClientSize.h = h;
130             maxClientSize.h = h;
131             size.h = h;
132             replaceMode = value;
133          }
134       }
135    }
136
137    void AddProjectItem(Project project)
138    {
139       if(created)
140       {
141          char label[MAX_FILENAME];
142          DataRow row;
143          sprintf(label, $"%s Project", project.name);
144          row = findIn.AddString(label);
145          row.tag = (int64)(intptr)project;
146       }
147    }
148
149    void RemoveProjectItem(Project project)
150    {
151       DataRow row;
152       if(inWorkspaceRow)
153       {
154          for(row = inWorkspaceRow.next; row; row = row.next)
155          {
156             if((Project)(intptr)row.tag == project)
157             {
158                findIn.DeleteRow(row);
159                break;
160             }
161          }
162       }
163    }
164
165    void Show()
166    {
167       if(!created)
168          Modal();
169       else
170          Activate();
171    }
172
173 private:
174
175    char currentDirectory[MAX_DIRECTORY];
176    FileFilter * filters;
177    int sizeFilters;
178    int fileFilter;
179    DataRow inDirectoryRow;
180    DataRow inWorkspaceRow;
181    FindInFilesMode lastSelectionMode;
182    String lastSelectionProject;
183    String lastSelectionProjectNode;
184    bool replaceMode;
185    SelectorButton starDir;
186
187    FindInFilesDialog()
188    {
189       GetWorkingDir(currentDirectory, MAX_DIRECTORY);
190       FileFixCase(currentDirectory);
191       findWhere.path = currentDirectory;
192       findWherePrjNode.AddField(projectNodeField);
193    }
194
195    ~FindInFilesDialog()
196    {
197       SearchStop();
198       delete lastSelectionProject;
199       delete lastSelectionProjectNode;
200    }
201
202    LayoutPage layout
203    {
204       this, anchor = { left = 20, top = 4, right = 8, bottom = 0 };
205       direction = horizontal;
206       tabCycle = true;
207       reverse = true;
208       gap = 8;
209    };
210
211    LayoutPage lpmain
212    {
213       layout, this;
214       tabCycle = true;
215       anchor.left = 0;
216       anchor.top = 0, anchor.bottom = 0;
217       gap = 2;
218    };
219
220    LayoutLine llfindIn      { lpmain, this, size.h = 26 };
221    LayoutLine llfindWhere   { lpmain, this, size.h = 26 };
222    LayoutLine llsubDirs     { lpmain, this, size.h = 18 };
223    LayoutLine llfilter      { lpmain, this, size.h = 26 };
224    LayoutLine llfileName    { lpmain, this, size.h = 26 };
225    LayoutLine llfindWhat    { lpmain, this, size.h = 26 };
226    LayoutLine llwholeWord   { lpmain, this, size.h = 18 };
227    LayoutLine llmatchCase   { lpmain, this, size.h = 18 };
228    LayoutLine llreplaceWith { lpmain, this, size.h = 26, visible = false };
229
230    Label lfindIn { llfindIn, this, size.w = 72, labeledWindow = findIn };
231    DropBox findIn
232    {
233       llfindIn, this, $"Find in:", altI, anchor.right = 0;
234
235       bool NotifySelect(DropBox control, DataRow row, Modifiers mods)
236       {
237          FindInFilesMode mode = this.mode;
238          bool inDir = mode == directory;
239          bool inWrk = mode == workspace;
240          bool inPrj = mode == project;
241          if(inPrj)
242             lfindWhere.labeledWindow = findWherePrjNode;
243          else
244             lfindWhere.labeledWindow = findWhere;
245          findWhere.visible = !inPrj;
246          findWhere.disabled = !inDir;
247          findWherePrjNode.visible = inPrj;
248          subDirs.disabled = inWrk;
249          objDirs.disabled = gitDirs.disabled = !inDir;
250          llfindWhere.size = { llfindWhere.size.w, llfindWhere.size.h };
251
252          if(row)
253          {
254             Project prj;
255             lastSelectionMode = mode;
256             prj = lastSelectionMode == project ? (Project)(intptr)row.tag : null;
257             delete lastSelectionProject;
258             if(prj)
259             {
260                DataRow r = null;
261                ProjectNode node = prj.topNode;
262                char filePath[MAX_LOCATION];
263                prj.topNode.GetFullFilePath(filePath);
264                lastSelectionProject = CopyString(filePath);
265                findWherePrjNode.Clear();
266                ListProjectNodeFolders(node, null);
267
268                if(lastSelectionProjectNode && !(node = prj.topNode.FindByFullPath(lastSelectionProjectNode, false)))
269                {
270                   node = prj.topNode;
271                   delete lastSelectionProjectNode;
272                }
273
274                for(r = findWherePrjNode.firstRow; r; r = r.next)
275                   if((ProjectNode)(intptr)r.tag == node)
276                      break;
277                if(r)
278                   findWherePrjNode.SelectRow(r);
279             }
280          }
281          return true;
282       }
283    };
284
285    void ListProjectNodeFolders(ProjectNode node, DataRow parentRow)
286    {
287       DataRow row;
288       if(parentRow)
289          row = findWherePrjNode/*parentRow*/.AddRow();
290       else
291          row = findWherePrjNode.AddRow();
292       row.tag = (int64)(intptr)node;
293       row.SetData(null, node);
294       if(node.files)
295       {
296          for(child : node.files; child.type == folder/* || child.type == file*//* || child.type == folderOpen*/)
297             ListProjectNodeFolders(child, row);
298       }
299    }
300
301    Label lfindWhere { llfindWhere, this, size.w = 72, labeledWindow = findWhere };
302    PathBox findWhere
303    {
304       llfindWhere, this, $"Find where:", altH, size.h = 24, anchor.right = 0;
305       typeExpected = directory, browseDialog = fileDialog;
306    };
307    DropBox findWherePrjNode
308    {
309       llfindWhere, this, $"Find where:", altH, size.h = 24, anchor.right = 0;
310       visible = false;
311       //collapseControl = true, treeBranches = true;
312
313       bool NotifySelect(DropBox control, DataRow row, Modifiers mods)
314       {
315          if(row)
316          {
317             ProjectNode node = (ProjectNode)(intptr)row.tag;
318             delete lastSelectionProjectNode;
319             if(node)
320             {
321                char filePath[MAX_LOCATION];
322                node.GetFullFilePath(filePath);
323                lastSelectionProjectNode = CopyString(filePath);
324             }
325          }
326          return true;
327       }
328    };
329    DataField projectNodeField { dataType = "ProjectNode", freeData = false };
330
331    Window spacerA { llsubDirs, this, size = { 72, 10 }, clickThrough = true, background = formColor, inactive = true };
332    Button subDirs
333    {
334       llsubDirs, this, $"Include Subdirectories", altU, isCheckbox = true, checked = true;
335    };
336    Button objDirs
337    {
338       llsubDirs, this, $"Include 'obj' directories", altO, isCheckbox = true, checked = false;
339    };
340    Button gitDirs
341    {
342       llsubDirs, this, $"Include '.git' directories", altG, isCheckbox = true, checked = false;
343    };
344
345    Label lfilter { llfilter, this, size.w = 72, labeledWindow = filterDrop };
346    DropBox filterDrop
347    {
348       llfilter, this, $"Filter:", altL, anchor.right = 0;
349
350       bool NotifySelect(DropBox control, DataRow row, Modifiers mods)
351       {
352          fileFilter = (int)(row ? row.tag : 0);
353          //ListFiles();
354          return true;
355       }
356    };
357
358    Label lfileName { llfileName, this, size.w = 72, labeledWindow = fileName };
359    EditBox fileName
360    {
361       llfileName, this, $"File name:", altN, size.h = 24, anchor.right = 0;
362    };
363
364    //Window spacerX { ll, this, size = { 72, 10 }, clickThrough = true, background = formColor, inactive = true };
365    //Button nameWholeWord { ll, this, "Whole word only", AltO, isCheckbox = true };
366    //Window spacerX { llwholeWord, this, size = { 72, 10 }, clickThrough = true, background = formColor, inactive = true };
367    //Button nameMatchCase { ll, this, "Match case", altA, isCheckbox = true };
368
369    Label lfindContent { llfindWhat, this, size.w = 72, labeledWindow = findContent };
370    EditBox findContent
371    {
372       llfindWhat, this, $"Find what:", altT, size.h = 24, anchor.right = 0;
373    };
374
375    Label lreplaceWith { llreplaceWith, this, size.w = 72, labeledWindow = replaceWith };
376    EditBox replaceWith
377    {
378       llreplaceWith, this, $"Replace with:", altE;
379       size.h = 24, anchor.right = 0;
380    };
381
382    Window spacerB { llwholeWord, this, size = { 72, 10 }, clickThrough = true, background = formColor, inactive = true };
383    Button contentWholeWord { llwholeWord, this, $"Whole word only", altW, isCheckbox = true };
384
385    Window spacerC { llmatchCase, this, size = { 72, 10 }, clickThrough = true, background = formColor, inactive = true };
386    Button contentMatchCase { llmatchCase, this, $"Match case", altC, isCheckbox = true };
387
388    LayoutPage lpbuttons
389    {
390       layout, this;
391       tabCycle = true;
392       size.w = 80;
393       anchor.top = 0, anchor.bottom = 0;
394       gap = 16;
395    };
396
397    LayoutLine llspace   { lpbuttons, this, size.h = 2 };
398    LayoutLine llfind    { lpbuttons, this, size.h = 30 };
399    LayoutLine llcancel  { lpbuttons, this, size.h = 30 };
400
401    Button find
402    {
403       llfind, this, $"Find", altF, isDefault = true, size = { 80, 24 }, anchor.horz = 0, anchor.vert = 0, keyRepeat = true;
404
405       bool NotifyClicked(Button control, int x, int y, Modifiers mods)
406       {
407          const String findPath = findWhere.path;
408          if(findIn.currentRow == inDirectoryRow && !findPath[0])
409          {
410             findWhere.Activate();
411             MessageBox { type = ok, master = parent,
412                   text = text, contents = $"You must specify a search location." }.Modal();
413          }
414          else if(findIn.currentRow == inDirectoryRow && !FileExists(findPath))
415          {
416             findWhere.Activate();
417             MessageBox { type = ok, master = parent,
418                   text = text, contents = $"Search location does not exist. Please provide a valid location." }.Modal();
419          }
420          else if(!fileName.contents[0] && !findContent.contents[0])
421          {
422             findContent.Activate();
423             MessageBox { type = ok, master = parent,
424                   text = text, contents = $"Nothing to be found. Please specify at least one criteria." }.Modal();
425          }
426          else
427          {
428             findContent.Activate();
429             SearchStop();
430             SearchStart();
431          }
432          return true;
433       }
434    };
435
436    Button cancel
437    {
438       llcancel, this, $"Cancel", hotKey = { escape }, size = { 80, 24 }, anchor.horz = 0, anchor.vert = 0;
439
440       bool NotifyClicked(Button control, int x, int y, Modifiers mods)
441       {
442          findContent.Activate();
443          Destroy(0);
444          return true;
445       }
446    };
447
448    SearchThread searchThread { findDialog = this };
449    FileDialog fileDialog { master = this, type = selectDir, text = $"Select Search Location..." };
450
451    void OnDestroy()
452    {
453       findIn.Clear();
454       inDirectoryRow = null;
455       inWorkspaceRow = null;
456    }
457
458    bool OnPostCreate()
459    {
460       bool withWorkspace = ide.workspace != null;
461       DataRow row;
462       if(!inDirectoryRow)
463          inDirectoryRow = findIn.AddString($"Directory");
464       if(withWorkspace)
465       {
466          if(!inWorkspaceRow)
467             inWorkspaceRow = findIn.AddString($"Workspace");
468          for(prj : ide.workspace.projects)
469             AddProjectItem(prj);
470          if(lastSelectionProject)
471          {
472             for(row = findIn.firstRow; row; row = row.next)
473             {
474                char filePath[MAX_LOCATION];
475                Project p = (Project)(intptr)row.tag;
476                if(p)
477                {
478                   p.topNode.GetFullFilePath(filePath);
479                   if(!fstrcmp(filePath, lastSelectionProject))
480                      break;
481                }
482             }
483             if(row)
484                findIn.SelectRow(row);
485             else
486                delete lastSelectionProject;
487          }
488          if(!lastSelectionProject)
489          {
490             if(lastSelectionMode == project)
491             {
492                Project prj = ide.workspace.projects.firstIterator.data;
493                for(row = findIn.firstRow; row; row = row.next)
494                   if((Project)(intptr)row.tag == prj)
495                      break;
496                if(row)
497                   findIn.SelectRow(row);
498             }
499             else
500                property::mode = lastSelectionMode;
501          }
502       }
503       else
504       {
505          findIn.SelectRow(inDirectoryRow);
506       }
507
508       findIn.disabled = !withWorkspace;
509       /*disabled = findIn.currentRow != inDirectoryRow;
510
511       findWhere.disabled = disabled;
512       subDirs.disabled = disabled;*/
513
514       findContent.Activate();
515       return true;
516    }
517
518    void SearchStart()
519    {
520       searchThread.active = true;
521       searchThread.project = null;
522       searchThread.projectNode = null;
523       searchThread.subDirs = subDirs.checked;
524       searchThread.objDirs = objDirs.checked;
525       searchThread.gitDirs = gitDirs.checked;
526
527       if(findIn.currentRow == inDirectoryRow)
528       {
529          searchThread.mode = directory;
530          strcpy(searchThread.dir, findWhere.path);
531       }
532       else if(findIn.currentRow == inWorkspaceRow)
533       {
534          searchThread.mode = workspace;
535          searchThread.subDirs = true;
536       }
537       else
538       {
539          searchThread.mode = project;
540          searchThread.project = (Project)(intptr)findIn.currentRow.tag;
541          searchThread.projectNode = (ProjectNode)(findWherePrjNode.currentRow ? (void *)(intptr)findWherePrjNode.currentRow.tag : null);
542       }
543       //searchThread.nameMatchCase = nameMatchCase.checked;
544       //searchThread.nameWholeWord = nameWholeWord.checked;
545       searchThread.contentMatchCase = contentMatchCase.checked;
546       searchThread.contentWholeWord = contentWholeWord.checked;
547
548       searchThread.filter = filters[fileFilter];
549
550       strcpy(searchThread.nameCriteria, fileName.contents);
551       strcpy(searchThread.contentCriteria, findContent.contents);
552       if(replaceMode)
553          strcpy(searchThread.contentReplace, replaceWith.contents);
554       searchThread.replaceMode = replaceMode;
555
556       //cancel.text = "Stop";
557
558       ide.outputView.ShowClearSelectTab(find);
559
560       Destroy(0);
561
562       searchThread.Create();
563    }
564
565    bool SearchAbort()
566    {
567       if(searchThread.active)
568       {
569          searchThread.Abort();
570          return true;
571       }
572       return false;
573    }
574
575    bool SearchStop()
576    {
577       if(searchThread.active)
578       {
579          searchThread.Abort();
580          searchThread.Abort();
581          app.Unlock();
582             searchThread.Wait();
583          app.Lock();
584          return true;
585       }
586       return false;
587    }
588
589    void SearchComplete()
590    {
591       //cancel.text = $"Cancel";
592    }
593
594    int GetSizeFilter()
595    {
596       return sizeFilters;
597    }
598
599    bool NotifyCharsAddedDeletedNameContent(EditBox editBox, BufferLocation before, BufferLocation after, bool pasteOperation)
600    {
601       find.disabled = (strlen(fileName.contents) == 0 && strlen(findContent.contents) == 0);
602       return true;
603    }
604
605    bool OnKeyHit(Key key, unichar ch)
606    {
607       if(ch && !key.alt && !key.ctrl && !key.shift && (contentMatchCase.active || contentWholeWord.active))
608       {
609          findContent.Activate();
610          return findContent.OnKeyHit(key, ch);
611       }
612       return true;
613    }
614 }
615
616 static define stackSize = 1024;
617
618 class SearchThread : Thread
619 {
620 public:
621    bool active, subDirs, objDirs, gitDirs/*, nameMatchCase, nameWholeWord*/, contentMatchCase, contentWholeWord;
622    char dir[MAX_DIRECTORY], contentCriteria[1024], contentReplace[1024], nameCriteria[1024];
623    FileFilter filter;
624    FindInFilesDialog findDialog;
625    FindInFilesMode mode;
626    Project project;
627    ProjectNode projectNode;
628    bool replaceMode;
629
630    void Abort()
631    {
632       if(abort)
633          abortNow = true;
634       else
635          abort = true;
636    }
637
638 private:
639
640    bool abort, abortNow;
641
642    SearchThread()
643    {
644       active = false;
645       abort = false;
646       abortNow = false;
647    }
648
649    int HowManyDigits(int num)
650    {
651       int count = 1;
652       while(num > 9)
653       {
654          count++;
655          num /= 10;
656       }
657       return count;
658    }
659
660    unsigned int Main()
661    {
662       int frame;
663       int globalFindCount = 0, filesSearchedCount = 0, filesMatchedCount = 0, dirsMatchedCount = 0;
664       //double lastTime = GetTime();
665       SearchStackFrame stack[stackSize];
666       FindInFilesMode mode = this.mode;
667
668       EditBox replaceEdit = null;
669
670       abort = false;
671       abortNow = false;
672
673       app.Lock();
674          {
675             char substring[512];
676             char containing[512];
677             const char * and;
678             const char * filterName;
679             if(!strcmp(filter.name, "All files"))
680                filterName = "files";
681             else
682                filterName = filter.name;
683             if(nameCriteria[0])
684                sprintf(substring, $" with file name matching \"%s\"", nameCriteria);
685             else
686                substring[0] = '\0';
687             if(contentCriteria && contentCriteria[0])
688                sprintf(containing, $" containing \"%s\"", contentCriteria);
689             else
690                containing[0] = '\0';
691             if(substring[0] && containing[0])
692                and = " and";
693             else
694                and = "";
695             if(mode == directory)
696             {
697                char * s;
698                ide.outputView.findBox.Logf(
699                      $"Searching \"%s\"%s for %s%s%s%s\n\n",
700                      (s = CopySystemPath(dir)), subDirs ? $" and its sub directories" : "",
701                      filterName, substring, and, containing);
702                delete s;
703             }
704             else if(mode == workspace)
705                ide.outputView.findBox.Logf(
706                      $"Searching workspace files for %s%s%s%s\n\n",
707                      filterName, substring, and, containing);
708             else if(mode == project)
709                ide.outputView.findBox.Logf(
710                      $"Searching project %s files for %s%s%s%s\n\n",
711                      project.name, filterName, substring, and, containing);
712          }
713       app.Unlock();
714
715       if(replaceMode)
716       {
717          replaceEdit = EditBox
718          {
719             multiLine = true,textHorzScroll = true,textVertScroll = true,
720             text = $"Replacing Editbox", size = Size { 640,480 }/*,maxLineSize = 65536*/
721          };
722       }
723
724       if(mode == directory)
725       {
726          strcpy(stack[0].path, dir);
727          stack[0].fileList = FileListing { dir, extensions = filter.extensions };  // there should be a sorted = true/false
728
729          for(frame = 0; frame >= 0 && frame < stackSize && !abort; )
730          {
731             if(stack[frame].fileList.Find())
732             {
733                bool match = true;
734                if(nameCriteria[0])
735                {
736                   char name[MAX_LOCATION];
737                   GetLastDirectory(stack[frame].fileList.path, name);
738                   if(SearchString(name, 0, nameCriteria, false, false) == null)
739                      match = false;
740                }
741                if(!stack[frame].fileList.stats.attribs.isDirectory)
742                {
743                   bool relative = false;
744                   char fileRelative[MAX_LOCATION];
745                   if(filter.ValidateFileName(stack[frame].fileList.name))
746                   {
747                      MakePathRelative(stack[frame].fileList.path, dir, fileRelative);
748                      relative = true;
749
750                      filesSearchedCount++;
751                      if(match && contentCriteria[0])
752                      {
753                         int ret;
754
755                         app.Lock();
756                            ide.outputView.findBox.Tellf(
757                                  $"Searching %s for %s", relative ? fileRelative : stack[frame].fileList.path, contentCriteria);
758                         app.Unlock();
759
760                         if(replaceMode)
761                            ret = SearchFileContentAndReplace(stack[frame].fileList.path, relative, fileRelative, replaceEdit);
762                         else
763                            ret = SearchFileContent(stack[frame].fileList.path, relative, fileRelative);
764                         if(ret > 0)
765                         {
766                            globalFindCount += ret;
767                            filesMatchedCount++;
768                         }
769                      }
770                      else if(match && nameCriteria[0])
771                      {
772                         filesMatchedCount++;
773                         app.Lock();
774                            ide.outputView.findBox.Logf(
775                                  "%s\n",
776                                  relative ? fileRelative : stack[frame].fileList.path);
777                         app.Unlock();
778                      }
779                   }
780                }
781                else
782                {
783                   bool relative = false;
784                   char fileRelative[MAX_LOCATION];
785                   MakePathRelative(stack[frame].fileList.path, dir, fileRelative);
786                   relative = true;
787                   app.Lock();
788                      if(match && nameCriteria[0])
789                      {
790                         dirsMatchedCount++;
791                         ide.outputView.findBox.Logf(
792                               "%s\n",
793                               relative ? fileRelative : stack[frame].fileList.path);
794                      }
795                      ide.outputView.findBox.Tellf(
796                            $"Searching %s", relative ? fileRelative : stack[frame].fileList.path);
797                   app.Unlock();
798                }
799
800                if(subDirs && stack[frame].fileList.stats.attribs.isDirectory &&
801                      (objDirs || strcmp(stack[frame].fileList.name, "obj")) &&
802                      (gitDirs || strcmp(stack[frame].fileList.name, ".git")))
803                {
804                   int lastFrame = frame;
805                   /*double thisTime = GetTime();
806                   app.Lock();
807                      if(thisTime - lastTime > 0.25)
808                      {
809                         findDialog.SearchUpdateLabel(stack[lastFrame].fileList.path);
810                         lastTime = thisTime;
811                      }
812                   app.Unlock();*/
813                   if(frame < stackSize-1)
814                   {
815                      frame++;
816                      strcpy(stack[frame].path, stack[lastFrame].fileList.path);
817                      stack[frame].fileList = FileListing { stack[frame].path, extensions = stack[lastFrame].fileList.extensions };
818                   }
819                   else
820                   {
821                      abort = true;
822                      for( ; frame >= 0 ; frame--)
823                         stack[frame].fileList.Stop();
824                      app.Lock();
825                         ide.outputView.findBox.Logf($"Error: aborting search!\n");
826                      app.Unlock();
827                   }
828                }
829             }
830             else
831             {
832                frame--;
833             }
834          }
835          if(abort)
836             for( ; frame >= 0 ; frame--)
837                stack[frame].fileList.Stop();
838       }
839       else if(mode == workspace || mode == project)
840       {
841          bool firtIteration = true;
842          Project prj = project;
843          ProjectNode stack[1024];
844          Iterator<Project> it { ide.workspace.projects };
845
846          while(true)
847          {
848             if(mode == workspace)
849             {
850                if(!it.Next()) break;
851                prj = it.data;
852             }
853             stack[1] = projectNode ? projectNode : prj.topNode;
854
855             for(frame = 1; frame && !abort;)
856             {
857                switch(stack[frame].type)
858                {
859                   case project:
860                      if((subDirs || firtIteration) && stack[frame].files && stack[frame].files.count)
861                      {
862                         int lastFrame = frame;
863                         frame++;
864                         stack[frame] = stack[lastFrame].files.first;
865                         firtIteration = false;
866                      }
867                      break;
868                   case file:
869                   {
870                      bool relative = true;
871                      char fileRelative[MAX_LOCATION];
872                      char filePath[MAX_LOCATION];
873                      filePath[0] = '\0';
874                      PathCat(filePath, prj.topNode.path);
875                      PathCat(filePath, stack[frame].path);
876                      PathCat(filePath, stack[frame].name);
877                      fileRelative[0] = '\0';
878                      PathCat(fileRelative, stack[frame].path);
879                      PathCat(fileRelative, stack[frame].name);
880                      if(relative && mode == workspace && prj != ide.project)
881                      {
882                         char special[MAX_LOCATION];
883                         sprintf(special, "(%s)%s", prj.name, fileRelative);
884                         strcpy(fileRelative, special);
885                      }
886                      if(filter.ValidateFileName(stack[frame].name))
887                      {
888                         filesSearchedCount++;
889                         if(!nameCriteria[0] || SearchString(stack[frame].name, 0, nameCriteria, false, false) != null)
890                         {
891                            if(contentCriteria[0])
892                            {
893                               int ret;
894
895                               app.Lock();
896                                  ide.outputView.findBox.Tellf(
897                                        $"Searching %s for \"%s\"", relative ? fileRelative : filePath,
898                                        contentCriteria);
899                               app.Unlock();
900
901                               if(replaceMode)
902                                  ret = SearchFileContentAndReplace(filePath, relative, fileRelative, replaceEdit);
903                               else
904                                  ret = SearchFileContent(filePath, relative, fileRelative);
905                               if(ret > 0)
906                               {
907                                  globalFindCount += ret;
908                                  filesMatchedCount++;
909                               }
910                            }
911                            else if(nameCriteria[0])
912                            {
913                               filesMatchedCount++;
914                               app.Lock();
915                                  ide.outputView.findBox.Logf(
916                                        "%s\n", relative ? fileRelative : filePath);
917                               app.Unlock();
918                            }
919                         }
920                      }
921                      stack[frame] = stack[frame].next;
922                      break;
923                   }
924                   case folder:
925                   {
926                      bool relative = true;
927                      char fileRelative[MAX_LOCATION];
928                      char filePath[MAX_LOCATION];
929                      filePath[0] = '\0';
930                      PathCat(filePath, prj.topNode.path);
931                      PathCat(filePath, stack[frame].path);
932                      fileRelative[0] = '\0';
933                      PathCat(fileRelative, stack[frame].path);
934                      if(relative && mode == workspace && prj != ide.project)
935                      {
936                         char special[MAX_LOCATION];
937                         sprintf(special, "(%s)%s", prj.name, fileRelative);
938                         strcpy(fileRelative, special);
939                      }
940                      if(nameCriteria[0] && SearchString(stack[frame].name, 0, nameCriteria, false, false) != null)
941                      {
942                         dirsMatchedCount++;
943                         app.Lock();
944                            ide.outputView.findBox.Logf(
945                                  "%s\n", relative ? fileRelative : filePath);
946                         app.Unlock();
947                      }
948                      if((subDirs || firtIteration) && stack[frame].files && stack[frame].files.count)
949                      {
950                         int lastFrame = frame;
951                         frame++;
952                         stack[frame] = stack[lastFrame].files.first;
953                         firtIteration = false;
954                      }
955                      else
956                         stack[frame] = stack[frame].next;
957                      break;
958                   }
959                   case resources:
960                      stack[frame] = stack[frame].next;
961                      break;
962                }
963                while(frame && !stack[frame])
964                {
965                   frame--;
966                   if(frame == 1 && projectNode && stack[frame] == projectNode)
967                      stack[frame] = null;
968                   else if(frame)
969                      stack[frame] = stack[frame].next;
970                }
971             }
972             if(mode == project) break;
973          }
974          if(abort)
975          {
976             for( ; frame ; frame--)
977                stack[frame] = null;
978          }
979       }
980       delete replaceEdit;
981
982       active = false;
983
984       app.Lock();
985          if(filesSearchedCount)
986          {
987             if(!contentCriteria[0] && (filesMatchedCount || dirsMatchedCount))
988                ide.outputView.findBox.Logf("\n");
989             if(globalFindCount)
990                ide.outputView.findBox.Logf(
991                      $"%s search %s a total of %d match%s in %d out of the %d file%s searched\n",
992                      abort ? $"Aborted" : $"Completed", replaceMode ? $"replaced" : $"found", globalFindCount, (globalFindCount > 1) ? $"es" : "",
993                      filesMatchedCount, filesSearchedCount, (filesSearchedCount > 1) ? $"s" : "");
994             else if(filesMatchedCount)
995                ide.outputView.findBox.Logf(
996                      $"%s search found a total of %d match%s in the %d file%s searched\n",
997                      abort ? $"Aborted" : $"Completed", filesMatchedCount, (filesMatchedCount > 1) ? $"es" : "",
998                      filesSearchedCount, (filesSearchedCount > 1) ? $"s" : "");
999             else
1000                ide.outputView.findBox.Logf(
1001                      $"%s search did not find any match in the %d files searched\n",
1002                      abort ? $"Aborted" : $"Completed", filesSearchedCount);
1003          }
1004          else
1005             ide.outputView.findBox.Logf(
1006                   $"%s search did not find any file\n", abort ? $"Aborted" : $"Completed");
1007          findDialog.SearchComplete();
1008       app.Unlock();
1009       return 0;
1010    }
1011
1012    int SearchFileContent(const char *filePath, bool relative, const char *fileRelative)
1013    {
1014       int findCount = -1;
1015       File f = FileOpen(filePath, read);
1016       if(f)
1017       {
1018          int lineNum = 0;
1019          char line[65536];
1020          findCount = 0;
1021          while(f.GetLine(line, 65536/* should there be a - 1 here? */) && !abortNow)
1022          {
1023             int col = 0;
1024             uint start = 0;
1025             char * find = null;
1026             int inLineFindCount = 0;
1027             lineNum++;
1028             while((find = SearchString(line, start, contentCriteria, contentMatchCase, contentWholeWord)) && !abortNow)
1029             {
1030                if(!col)
1031                   col = find - &line[start] + 1;
1032                start += (find - &line[start]) / sizeof(char) + strlen(contentCriteria);
1033                inLineFindCount++;
1034             }
1035             if(inLineFindCount && !abortNow)
1036             {
1037                char s1[7] = "      ";
1038                char s2[5] = "    ";
1039                int len = strlen(line);
1040                s1[6 - HowManyDigits(lineNum)] = '\0';
1041                s2[4 - HowManyDigits(col)] = '\0';
1042                // Cut the line longer than 1024 because Logf prints to a buffer (and we don't want to output crazy long lines either)
1043                if(len > 1023)
1044                {
1045                   line[1023] = '\0';
1046                   line[1022] = '.';
1047                   line[1021] = '.';
1048                   line[1020] = '.';
1049                }
1050                app.Lock();
1051                   ide.outputView.findBox.Logf(
1052                         "   %s:%d:%d%s%s> %s\n", relative ? fileRelative : filePath,
1053                         lineNum, col, s1, s2, line);
1054                app.Unlock();
1055                findCount += inLineFindCount;
1056             }
1057             // todo
1058             /*else
1059                f.Seek(-strlen(contentCriteria), current);*/
1060          }
1061          delete f;
1062          if(findCount)
1063          {
1064             app.Lock();
1065                ide.outputView.findBox.Logf(
1066                      $"Found %d match%s in \"%s\"%s\n\n", findCount, (findCount > 1) ? "es" : "",
1067                      relative ? fileRelative : filePath, abortNow ? $" before search was aborted" : "");
1068             app.Unlock();
1069          }
1070       }
1071       else
1072       {
1073          app.Lock();
1074             ide.outputView.findBox.Logf($"Unable to open file %s\n\n", filePath);
1075          app.Unlock();
1076       }
1077       return findCount;
1078    }
1079
1080    int SearchFileContentAndReplace(const char *filePath, bool relative, const char *fileRelative, EditBox edit)
1081    {
1082       int replaceCount = -1;
1083       File f = FileOpen(filePath, read);
1084       if(f)
1085       {
1086          char line[65536];
1087          replaceCount = 0;
1088          while(f.GetLine(line, 65536/* should there be a - 1 here? */) && !abortNow)
1089          {
1090             int col = 0;
1091             if(SearchString(line, 0, contentCriteria, contentMatchCase, contentWholeWord) && !abortNow)
1092             {
1093                int lastLineNum = 0;
1094                f.Seek(0, start);
1095                edit.Load(f);
1096                delete f;
1097
1098                for(; edit.Find(contentCriteria, contentWholeWord, contentMatchCase, true) == found; replaceCount++)
1099                {
1100                   int lineNum = edit.lineNumber + 1;
1101                   edit.PutS(contentReplace);
1102                   if(lineNum != lastLineNum)
1103                   {
1104                      int len;
1105                      char line[1024];
1106                      char s1[6] = "      ";
1107                      char s2[4] = "    ";
1108                      //int len = strlen(line);
1109                      s1[6 - HowManyDigits(lineNum)] = '\0';
1110                      s2[4 - HowManyDigits(col)] = '\0';
1111                      strncpy(line, edit.line.text, 1023);
1112                      line[1023] = '\0';
1113                      len = strlen(line);
1114                      // Cut the line longer than 1024 because Logf prints
1115                      // to a buffer (and we don't want to output crazy long lines either)
1116                      if(len > 1023)
1117                      {
1118                         line[1022] = '.';
1119                         line[1021] = '.';
1120                         line[1020] = '.';
1121                      }
1122                      app.Lock();
1123                         ide.outputView.findBox.Logf(
1124                               "   %s:%d:%d%s%s> %s\n", relative ? fileRelative : filePath,
1125                               lineNum, col, s1, s2, line);
1126                      app.Unlock();
1127                   }
1128                }
1129                if(replaceCount)
1130                {
1131                   if((f = FileOpen(filePath, write)))
1132                   {
1133                      edit.Save(f, false);
1134                      delete f;
1135                   }
1136                }
1137                edit.Clear();
1138                break;
1139             }
1140             // todo
1141             /*else
1142                f.Seek(-strlen(contentCriteria), current);*/
1143
1144          }
1145          delete f;
1146          if(replaceCount)
1147          {
1148             app.Lock();
1149                ide.outputView.findBox.Logf(
1150                      $"Replaced %d match%s in \"%s\"%s\n\n", replaceCount, (replaceCount > 1) ? $"es" : "",
1151                      relative ? fileRelative : filePath, abortNow ? $" before search was aborted" : "");
1152             app.Unlock();
1153          }
1154       }
1155       else
1156       {
1157          app.Lock();
1158             ide.outputView.findBox.Logf($"Unable to open file %s\n\n", filePath);
1159          app.Unlock();
1160       }
1161       return replaceCount;
1162    }
1163 }
1164
1165 static struct SearchStackFrame
1166 {
1167    int tag;
1168    char path[MAX_LOCATION];
1169    FileListing fileList;
1170 };