ide: fix lack of support of variables in node path for find in files.
[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, 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, 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, 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       FindInFilesMode mode = this.mode;
655
656       EditBox replaceEdit = null;
657
658       abort = false;
659       abortNow = false;
660
661       app.Lock();
662          {
663             char substring[512];
664             char containing[512];
665             const char * and;
666             const char * filterName;
667             if(!strcmp(filter.name, "All files"))
668                filterName = "files";
669             else
670                filterName = filter.name;
671             if(nameCriteria[0])
672                sprintf(substring, $" with file name matching \"%s\"", nameCriteria);
673             else
674                substring[0] = '\0';
675             if(contentCriteria && contentCriteria[0])
676                sprintf(containing, $" containing \"%s\"", contentCriteria);
677             else
678                containing[0] = '\0';
679             if(substring[0] && containing[0])
680                and = " and";
681             else
682                and = "";
683             if(mode == directory)
684             {
685                char * s;
686                ide.outputView.findBox.Logf(
687                      $"Searching \"%s\"%s for %s%s%s%s\n\n",
688                      (s = CopySystemPath(dir)), subDirs ? $" and its sub directories" : "",
689                      filterName, substring, and, containing);
690                delete s;
691             }
692             else if(mode == workspace)
693                ide.outputView.findBox.Logf(
694                      $"Searching workspace files for %s%s%s%s\n\n",
695                      filterName, substring, and, containing);
696             else if(mode == project)
697                ide.outputView.findBox.Logf(
698                      $"Searching project %s files for %s%s%s%s\n\n",
699                      project.name, filterName, substring, and, containing);
700          }
701       app.Unlock();
702
703       if(replaceMode)
704       {
705          replaceEdit = EditBox
706          {
707             multiLine = true,textHorzScroll = true,textVertScroll = true,
708             text = $"Replacing Editbox", size = Size { 640,480 }/*,maxLineSize = 65536*/
709          };
710       }
711
712       if(mode == directory)
713       {
714          SearchStackFrame * stack = new0 SearchStackFrame[stackSize];
715
716          strcpy(stack[0].path, dir);
717          stack[0].fileList = FileListing { dir, extensions = filter.extensions };  // there should be a sorted = true/false
718
719          for(frame = 0; frame >= 0 && frame < stackSize && !abort; )
720          {
721             if(stack[frame].fileList.Find())
722             {
723                bool match = true;
724                if(nameCriteria[0])
725                {
726                   char name[MAX_LOCATION];
727                   GetLastDirectory(stack[frame].fileList.path, name);
728                   if(SearchString(name, 0, nameCriteria, false, false) == null)
729                      match = false;
730                }
731                if(!stack[frame].fileList.stats.attribs.isDirectory)
732                {
733                   bool relative = false;
734                   char fileRelative[MAX_LOCATION];
735                   if(filter.ValidateFileName(stack[frame].fileList.name))
736                   {
737                      MakePathRelative(stack[frame].fileList.path, dir, fileRelative);
738                      relative = true;
739
740                      filesSearchedCount++;
741                      if(match && contentCriteria[0])
742                      {
743                         int ret;
744
745                         app.Lock();
746                            ide.outputView.findBox.Tellf(
747                                  $"Searching %s for %s", relative ? fileRelative : stack[frame].fileList.path, contentCriteria);
748                         app.Unlock();
749
750                         if(replaceMode)
751                            ret = SearchFileContentAndReplace(stack[frame].fileList.path, relative, fileRelative, replaceEdit);
752                         else
753                            ret = SearchFileContent(stack[frame].fileList.path, relative, fileRelative);
754                         if(ret > 0)
755                         {
756                            globalFindCount += ret;
757                            filesMatchedCount++;
758                         }
759                      }
760                      else if(match && nameCriteria[0])
761                      {
762                         filesMatchedCount++;
763                         app.Lock();
764                            ide.outputView.findBox.Logf(
765                                  "%s\n",
766                                  relative ? fileRelative : stack[frame].fileList.path);
767                         app.Unlock();
768                      }
769                   }
770                }
771                else
772                {
773                   bool relative = false;
774                   char fileRelative[MAX_LOCATION];
775                   MakePathRelative(stack[frame].fileList.path, dir, fileRelative);
776                   relative = true;
777                   app.Lock();
778                      if(match && nameCriteria[0])
779                      {
780                         dirsMatchedCount++;
781                         ide.outputView.findBox.Logf(
782                               "%s\n",
783                               relative ? fileRelative : stack[frame].fileList.path);
784                      }
785                      ide.outputView.findBox.Tellf(
786                            $"Searching %s", relative ? fileRelative : stack[frame].fileList.path);
787                   app.Unlock();
788                }
789
790                if(subDirs && stack[frame].fileList.stats.attribs.isDirectory && strcmp(stack[frame].fileList.name, ".git"))
791                {
792                   int lastFrame = frame;
793                   /*double thisTime = GetTime();
794                   app.Lock();
795                      if(thisTime - lastTime > 0.25)
796                      {
797                         findDialog.SearchUpdateLabel(stack[lastFrame].fileList.path);
798                         lastTime = thisTime;
799                      }
800                   app.Unlock();*/
801                   if(frame < stackSize-1)
802                   {
803                      frame++;
804                      strcpy(stack[frame].path, stack[lastFrame].fileList.path);
805                      stack[frame].fileList = FileListing { stack[frame].path, extensions = stack[lastFrame].fileList.extensions };
806                   }
807                   else
808                   {
809                      abort = true;
810                      for( ; frame >= 0 ; frame--)
811                         stack[frame].fileList.Stop();
812                      app.Lock();
813                         ide.outputView.findBox.Logf($"Error: aborting search!\n");
814                      app.Unlock();
815                   }
816                }
817             }
818             else
819             {
820                frame--;
821             }
822          }
823          if(abort)
824             for( ; frame >= 0 ; frame--)
825                if(frame < stackSize)
826                   stack[frame].fileList.Stop();
827          delete stack;
828       }
829       else if(mode == workspace || mode == project)
830       {
831          bool firstIteration = true;
832          Project prj = project;
833          ProjectNode * stack = new0 ProjectNode[stackSize];
834          Iterator<Project> it { ide.workspace.projects };
835
836          while(true)
837          {
838             if(mode == workspace)
839             {
840                if(!it.Next()) break;
841                prj = it.data;
842             }
843             stack[1] = projectNode ? projectNode : prj.topNode;
844
845             for(frame = 1; frame && !abort && frame < stackSize-1;)
846             {
847                bool relative = true; // would be option
848                char fileRelative[MAX_LOCATION];
849                char filePath[MAX_LOCATION];
850                filePath[0] = '\0';
851                fileRelative[0] = '\0';
852                if(stack[frame].type == file || stack[frame].type == folder || stack[frame].type == folderOpen)
853                {
854                   stack[frame].GetFullFilePath(filePath, true, true);
855                   //MakePathRelative(filePath, prj.topNode.path, fileRelative);
856                   MakePathRelative(filePath, prj.topNode.path, fileRelative);
857                }
858                switch(stack[frame].type)
859                {
860                   case project:
861                      if((subDirs || firstIteration) && stack[frame].files && stack[frame].files.count)
862                      {
863                         int lastFrame = frame;
864                         frame++;
865                         stack[frame] = stack[lastFrame].files.first;
866                         firstIteration = false;
867                      }
868                      break;
869                   case file:
870                   {
871                      if(relative && mode == workspace && prj != ide.project)
872                      {
873                         char special[MAX_LOCATION];
874                         sprintf(special, "(%s)%s", prj.name, fileRelative);
875                         strcpy(fileRelative, special);
876                      }
877                      if(filter.ValidateFileName(stack[frame].name))
878                      {
879                         filesSearchedCount++;
880                         if(!nameCriteria[0] || SearchString(stack[frame].name, 0, nameCriteria, false, false) != null)
881                         {
882                            if(contentCriteria[0])
883                            {
884                               int ret;
885
886                               app.Lock();
887                                  ide.outputView.findBox.Tellf(
888                                        $"Searching %s for \"%s\"", relative ? fileRelative : filePath,
889                                        contentCriteria);
890                               app.Unlock();
891
892                               if(replaceMode)
893                                  ret = SearchFileContentAndReplace(filePath, relative, fileRelative, replaceEdit);
894                               else
895                                  ret = SearchFileContent(filePath, relative, fileRelative);
896                               if(ret > 0)
897                               {
898                                  globalFindCount += ret;
899                                  filesMatchedCount++;
900                               }
901                            }
902                            else if(nameCriteria[0])
903                            {
904                               filesMatchedCount++;
905                               app.Lock();
906                                  ide.outputView.findBox.Logf(
907                                        "%s\n", relative ? fileRelative : filePath);
908                               app.Unlock();
909                            }
910                         }
911                      }
912                      stack[frame] = stack[frame].next;
913                      break;
914                   }
915                   case folder:
916                   {
917                      if(relative && mode == workspace && prj != ide.project)
918                      {
919                         char special[MAX_LOCATION];
920                         sprintf(special, "(%s)%s", prj.name, fileRelative);
921                         strcpy(fileRelative, special);
922                      }
923                      if(nameCriteria[0] && SearchString(stack[frame].name, 0, nameCriteria, false, false) != null)
924                      {
925                         dirsMatchedCount++;
926                         app.Lock();
927                            ide.outputView.findBox.Logf(
928                                  "%s\n", relative ? fileRelative : filePath);
929                         app.Unlock();
930                      }
931                      if((subDirs || firstIteration) && stack[frame].files && stack[frame].files.count)
932                      {
933                         int lastFrame = frame;
934                         frame++;
935                         stack[frame] = stack[lastFrame].files.first;
936                         firstIteration = false;
937                      }
938                      else
939                         stack[frame] = stack[frame].next;
940                      break;
941                   }
942                   case resources:
943                      stack[frame] = stack[frame].next;
944                      break;
945                }
946                while(frame && !stack[frame])
947                {
948                   frame--;
949                   if(frame == 1 && projectNode && stack[frame] == projectNode)
950                      stack[frame] = null;
951                   else if(frame)
952                      stack[frame] = stack[frame].next;
953                }
954             }
955             if(mode == project) break;
956          }
957          if(abort)
958          {
959             for( ; frame >= 0; frame--)
960                if(frame < stackSize)
961                   stack[frame] = null;
962          }
963          delete stack;
964       }
965       delete replaceEdit;
966
967       active = false;
968
969       app.Lock();
970          if(filesSearchedCount)
971          {
972             if(!contentCriteria[0] && (filesMatchedCount || dirsMatchedCount))
973                ide.outputView.findBox.Logf("\n");
974             if(globalFindCount)
975                ide.outputView.findBox.Logf(
976                      $"%s search %s a total of %d match%s in %d out of the %d file%s searched\n",
977                      abort ? $"Aborted" : $"Completed", replaceMode ? $"replaced" : $"found", globalFindCount, (globalFindCount > 1) ? $"es" : "",
978                      filesMatchedCount, filesSearchedCount, (filesSearchedCount > 1) ? $"s" : "");
979             else if(filesMatchedCount)
980                ide.outputView.findBox.Logf(
981                      $"%s search found a total of %d match%s in the %d file%s searched\n",
982                      abort ? $"Aborted" : $"Completed", filesMatchedCount, (filesMatchedCount > 1) ? $"es" : "",
983                      filesSearchedCount, (filesSearchedCount > 1) ? $"s" : "");
984             else
985                ide.outputView.findBox.Logf(
986                      $"%s search did not find any match in the %d files searched\n",
987                      abort ? $"Aborted" : $"Completed", filesSearchedCount);
988          }
989          else
990             ide.outputView.findBox.Logf(
991                   $"%s search did not find any file\n", abort ? $"Aborted" : $"Completed");
992          findDialog.SearchComplete();
993       app.Unlock();
994       return 0;
995    }
996
997    int SearchFileContent(const char *filePath, bool relative, const char *fileRelative)
998    {
999       int findCount = -1;
1000       File f = FileOpen(filePath, read);
1001       if(f)
1002       {
1003          int lineNum = 0;
1004          char line[65536];
1005          findCount = 0;
1006          while(f.GetLine(line, 65536/* should there be a - 1 here? */) && !abortNow)
1007          {
1008             int col = 0;
1009             uint start = 0;
1010             char * find = null;
1011             int inLineFindCount = 0;
1012             lineNum++;
1013             while((find = SearchString(line, start, contentCriteria, contentMatchCase, contentWholeWord)) && !abortNow)
1014             {
1015                if(!col)
1016                   col = find - &line[start] + 1;
1017                start += (find - &line[start]) / sizeof(char) + strlen(contentCriteria);
1018                inLineFindCount++;
1019             }
1020             if(inLineFindCount && !abortNow)
1021             {
1022                char s1[7] = "      ";
1023                char s2[5] = "    ";
1024                int len = strlen(line);
1025                s1[6 - HowManyDigits(lineNum)] = '\0';
1026                s2[4 - HowManyDigits(col)] = '\0';
1027                // Cut the line longer than 1024 because Logf prints to a buffer (and we don't want to output crazy long lines either)
1028                if(len > 1023)
1029                {
1030                   line[1023] = '\0';
1031                   line[1022] = '.';
1032                   line[1021] = '.';
1033                   line[1020] = '.';
1034                }
1035                app.Lock();
1036                   ide.outputView.findBox.Logf(
1037                         "   %s:%d:%d%s%s> %s\n", relative ? fileRelative : filePath,
1038                         lineNum, col, s1, s2, line);
1039                app.Unlock();
1040                findCount += inLineFindCount;
1041             }
1042             // todo
1043             /*else
1044                f.Seek(-strlen(contentCriteria), current);*/
1045          }
1046          delete f;
1047          if(findCount)
1048          {
1049             app.Lock();
1050                ide.outputView.findBox.Logf(
1051                      $"Found %d match%s in \"%s\"%s\n\n", findCount, (findCount > 1) ? "es" : "",
1052                      relative ? fileRelative : filePath, abortNow ? $" before search was aborted" : "");
1053             app.Unlock();
1054          }
1055       }
1056       else
1057       {
1058          app.Lock();
1059             ide.outputView.findBox.Logf($"Unable to open file %s\n\n", filePath);
1060          app.Unlock();
1061       }
1062       return findCount;
1063    }
1064
1065    int SearchFileContentAndReplace(const char *filePath, bool relative, const char *fileRelative, EditBox edit)
1066    {
1067       int replaceCount = -1;
1068       File f = FileOpen(filePath, read);
1069       if(f)
1070       {
1071          char line[65536];
1072          replaceCount = 0;
1073          while(f.GetLine(line, 65536/* should there be a - 1 here? */) && !abortNow)
1074          {
1075             int col = 0;
1076             if(SearchString(line, 0, contentCriteria, contentMatchCase, contentWholeWord) && !abortNow)
1077             {
1078                int lastLineNum = 0;
1079                f.Seek(0, start);
1080                edit.Load(f);
1081                delete f;
1082
1083                for(; edit.Find(contentCriteria, contentWholeWord, contentMatchCase, true) == found; replaceCount++)
1084                {
1085                   int lineNum = edit.lineNumber + 1;
1086                   edit.PutS(contentReplace);
1087                   if(lineNum != lastLineNum)
1088                   {
1089                      int len;
1090                      char line[1024];
1091                      char s1[6] = "      ";
1092                      char s2[4] = "    ";
1093                      //int len = strlen(line);
1094                      s1[6 - HowManyDigits(lineNum)] = '\0';
1095                      s2[4 - HowManyDigits(col)] = '\0';
1096                      strncpy(line, edit.line.text, 1023);
1097                      line[1023] = '\0';
1098                      len = strlen(line);
1099                      // Cut the line longer than 1024 because Logf prints
1100                      // to a buffer (and we don't want to output crazy long lines either)
1101                      if(len > 1023)
1102                      {
1103                         line[1022] = '.';
1104                         line[1021] = '.';
1105                         line[1020] = '.';
1106                      }
1107                      app.Lock();
1108                         ide.outputView.findBox.Logf(
1109                               "   %s:%d:%d%s%s> %s\n", relative ? fileRelative : filePath,
1110                               lineNum, col, s1, s2, line);
1111                      app.Unlock();
1112                   }
1113                }
1114                if(replaceCount)
1115                {
1116                   if((f = FileOpen(filePath, write)))
1117                   {
1118                      edit.Save(f, false);
1119                      delete f;
1120                   }
1121                }
1122                edit.Clear();
1123                break;
1124             }
1125             // todo
1126             /*else
1127                f.Seek(-strlen(contentCriteria), current);*/
1128
1129          }
1130          delete f;
1131          if(replaceCount)
1132          {
1133             app.Lock();
1134                ide.outputView.findBox.Logf(
1135                      $"Replaced %d match%s in \"%s\"%s\n\n", replaceCount, (replaceCount > 1) ? $"es" : "",
1136                      relative ? fileRelative : filePath, abortNow ? $" before search was aborted" : "");
1137             app.Unlock();
1138          }
1139       }
1140       else
1141       {
1142          app.Lock();
1143             ide.outputView.findBox.Logf($"Unable to open file %s\n\n", filePath);
1144          app.Unlock();
1145       }
1146       return replaceCount;
1147    }
1148 }
1149
1150 static struct SearchStackFrame
1151 {
1152    int tag;
1153    char path[MAX_LOCATION];
1154    FileListing fileList;
1155 };