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