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