ide: workspace: fix making changes in openedFiles list during iteration through same...
[sdk] / ide / src / project / Workspace.ec
1 import "ide"
2
3 enum OpenedFileState { unknown, opened, closed };
4 enum ValgrindLeakCheck
5 {
6    no, summary, yes, full;
7
8    property const char *
9    {
10       get { return OnGetString(null, null, null); }
11    }
12
13    const char * OnGetString(char * tempString, void * fieldData, bool * needClass)
14    {
15       if(this >= no && this <= full)
16       {
17          if(tempString)
18             strcpy(tempString, valgrindLeakCheckNames[this]);
19          return valgrindLeakCheckNames[this];
20       }
21       if(tempString && tempString[0])
22          tempString[0] = '\0';
23       return null;
24    }
25 };
26 static const char * valgrindLeakCheckNames[ValgrindLeakCheck] = { "no", "summary", "yes", "full" };
27
28 class OpenedFileInfo : struct
29 {
30    class_no_expansion;
31 public:
32 //   class_fixed
33    char * path;
34    OpenedFileState state;
35    int lineNumber;
36    int position;
37    Point scroll;
38    TimeStamp modified;
39
40 private:
41    bool holdTracking;
42
43    property bool trackingAllowed
44    {
45       get { return !holdTracking && ide && ide.workspace && !ide.workspace.holdTracking; }
46    }
47
48    void CaretMove(int line, int charPos)
49    {
50       if(trackingAllowed && (line != lineNumber || position != charPos))
51       {
52          lineNumber = line;
53          position = charPos;
54          ide.workspace.modified = true;
55       }
56    }
57
58    void ScrollChange(Point scroll)
59    {
60       if(trackingAllowed)
61       {
62          this.scroll.x = scroll.x;
63          this.scroll.y = scroll.y;
64          ide.workspace.modified = true;
65       }
66    }
67
68    void Activate()
69    {
70       if(trackingAllowed)
71       {
72          List<OpenedFileInfo> files = ide.workspace.openedFiles;
73          Iterator<OpenedFileInfo> it { files };
74          IteratorPointer last;
75          if(it.Find(this) && it.pointer != (last = files.GetLast()))
76          {
77             files.Move(it.pointer, last);
78             ide.workspace.modified = true;
79          }
80          modified = GetLocalTimeStamp();
81       }
82    }
83
84    void SetCodeEditorState(CodeEditor editor)
85    {
86       int num = Max(lineNumber - 1, 0);
87       Point scrl;
88       holdTracking = true;
89       if(editor.editBox.GoToLineNum(num))
90       {
91          int pos = Max(Min(editor.editBox.line.count, position - 1), 0);
92          editor.editBox.GoToPosition(editor.editBox.line, num, pos);
93       }
94       scrl.x = Max(scroll.x, 0);
95       scrl.y = Max(scroll.y, 0);
96       editor.editBox.scroll = scrl;
97       holdTracking = false;
98    }
99
100    ~OpenedFileInfo()
101    {
102       delete path;
103    }
104 };
105
106 class WorkspaceFile : struct
107 {
108    class_no_expansion;
109 public:
110    property const char * format
111    {
112       set { }
113       get { return "Ecere IDE Workspace File"; }
114       isset { return true; }
115    }
116    property const char * version
117    {
118       set { }
119       get { return "0.1"; }
120       isset { return true; }
121    }
122    Workspace workspace;
123 }
124
125 class AddedProjectInfo : struct
126 {
127    class_no_expansion;
128 public:
129    property const char * path
130    {
131       set { delete path; if(value && value[0]) path = CopyString(value); }
132       get { return path && path[0] ? path : null; }
133       isset { return path != null; }
134    }
135    property const char * activeConfig
136    {
137       set { delete activeConfig; if(value && value[0]) activeConfig = CopyString(value); }
138       get
139       {
140          char * config;
141          if(project && project.config)
142             config = project.config.name;
143          else
144             config = activeConfig;
145          return config && config[0] ? config : null;
146       }
147       isset { return property::activeConfig != null; }
148    }
149 private:
150    char * path;
151    char * activeConfig;
152    Project project;
153
154    void Free()
155    {
156       delete path;
157       delete activeConfig;
158    }
159
160    void OnFree()
161    {
162       Free();
163       class::OnFree();
164    }
165
166    ~AddedProjectInfo()
167    {
168       Free();
169    }
170 }
171
172 class Workspace : struct
173 {
174    class_no_expansion;
175 public:
176    property const char * name
177    {
178       set { delete name; if(value && value[0]) name = CopyString(value); }
179       get { return name && name[0] ? name : null; }
180       isset { return name != null; }
181    }
182    property const char * activeCompiler
183    {
184       set { delete activeCompiler; if(value && value[0]) activeCompiler = CopyString(value); }
185       get { return activeCompiler && activeCompiler[0] ? activeCompiler : null; }
186       isset { return activeCompiler != null; }
187    }
188    int bitDepth;
189    property const char * commandLineArgs
190    {
191       set { delete commandLineArgs; if(value && value[0]) commandLineArgs = CopyString(value); }
192       get { return commandLineArgs && commandLineArgs[0] ? commandLineArgs : null; }
193       isset { return commandLineArgs != null; }
194    }
195    property const char * debugDir
196    {
197       set { delete debugDir; if(value && value[0]) debugDir = CopyString(value); }
198       get { return debugDir && debugDir[0] ? debugDir : null; }
199       isset { return debugDir != null; }
200    }
201
202    property List<AddedProjectInfo> addedProjects { set { addedProjects = value; } get { return addedProjects; } isset { return addedProjects && addedProjects.count; } }
203    property List<String> sourceDirs { set { sourceDirs = value; } get { return sourceDirs; } isset { return sourceDirs && sourceDirs.count; } }
204    property Array<NamedString> environmentVars { set { environmentVars = value; } get { return environmentVars; } isset { return environmentVars && environmentVars.count; } }
205    property List<Breakpoint> breakpoints { set { breakpoints = value; } get { return breakpoints; } isset { return breakpoints && breakpoints.count; } }
206    property List<Watch> watches { set { watches = value; } get { return watches; } isset { return watches && watches.count; } }
207    property List<OpenedFileInfo> openedFiles { set { openedFiles = value; } get { return openedFiles; } isset { return openedFiles && openedFiles.count; } }
208
209    bool useValgrind;
210    ValgrindLeakCheck vgLeakCheck;
211    bool vgTrackOrigins;
212    int vgRedzoneSize;
213
214 private:
215    char * name;
216    char * activeCompiler;
217    char * commandLineArgs;
218    char * debugDir;
219
220    List<AddedProjectInfo> addedProjects;
221    List<String> sourceDirs;
222    Array<NamedString> environmentVars;
223    List<Breakpoint> breakpoints;
224    List<Watch> watches;
225    List<OpenedFileInfo> openedFiles;
226
227    char * workspaceFile;
228    char * workspaceDir;
229
230    int bpCount;
231
232    List<Project> projects { };
233    //Project project;
234
235    bool modified;
236    bool holdTracking;
237
238    vgRedzoneSize = -1;
239    vgLeakCheck = summary;
240
241    Timer timer
242    {
243       userData = this, delay = 1.0;
244       bool DelayExpired()
245       {
246          static bool skip = true;
247          if(skip)
248             skip = false;
249          else if(modified)
250             Save();
251
252          if(ide.debugStart)
253          {
254             ide.MenuDebugStart(ide.debugStartResumeItem, 0);
255             ide.debugStart = false;
256          }
257          return true;
258       }
259    };
260
261    void AddProject(Project project, AddedProjectInfo addedProject)
262    {
263       if(addedProject)
264       {
265          ProjectConfig activeConfig = project.GetConfig(addedProject.activeConfig);
266          if(activeConfig)
267             project.config = activeConfig;
268          addedProject.project = project;
269       }
270       else
271       {
272          char location[MAX_LOCATION];
273          GetRelativePathForProject(location, project);
274          if(!addedProjects)
275             addedProjects = { };
276          addedProjects.Add(AddedProjectInfo { path = location, project = project });
277       }
278       projects.Add(project);
279    }
280
281    property const char * workspaceFile
282    {
283       set
284       {
285          char dir[MAX_DIRECTORY];
286          if(workspaceFile) delete workspaceFile;
287          if(workspaceDir) delete workspaceDir;
288          workspaceFile = CopyString(value);
289          StripLastDirectory(workspaceFile, dir);
290          workspaceDir = CopyUnixPath(dir);
291       }
292       get { return workspaceFile; }
293    }
294
295    property const char * projectDir
296    {
297       get
298       {
299          if(projects.first)
300          {
301             Project prj = projects.firstIterator.data;
302             return prj.topNode ? prj.topNode.path : null;
303          }
304          else
305             return workspaceDir;
306       }
307    }
308
309    /*property Project project
310    {
311       set
312       {
313          if(project)
314          {
315          }
316          project = value;
317          if(project)
318          {
319             projectDir = CopyString(project.topNode.path);
320
321             if(!project.config && activeConfig && activeConfig[0])
322             {
323                ProjectConfig cfg;
324                for(cfg = project.configurations.first; cfg; cfg = cfg.next)
325                   if(!strcmp(cfg.name, activeConfig))
326                      break;
327                project.config = cfg;
328             }
329             if(!project.config)
330                project.config = project.configurations.first;
331          }
332       }
333       get { return project; }
334    }*/
335
336 public:
337    void Save()
338    {
339       if(workspaceFile && workspaceFile[0])
340       {
341          File file = FileOpen(workspaceFile, write);
342          if(file)
343          {
344             WorkspaceFile wf { workspace = this };
345             WriteECONObject(file, class(WorkspaceFile), wf, 0);
346             delete wf;
347             delete file;
348             modified = false;
349          }
350       }
351    }
352
353    char * CopyAbsolutePathFromRelative(const char * relative)
354    {
355       char name[MAX_LOCATION];
356       char absolute[MAX_LOCATION];
357       Project prj = null;
358       ProjectNode node = null;
359
360       GetLastDirectory(relative, name);
361       for(p : projects)
362       {
363          if((node = p.topNode.Find(name, false)))
364          {
365             prj = p;
366             break;
367          }
368       }
369       if(prj)
370       {
371          node.GetFullFilePath(absolute, true);
372          return CopyString(absolute);
373       }
374
375       prj = null;
376       for(p : projects)
377       {
378          strcpy(absolute, p.topNode.path);
379          PathCatSlash(absolute, relative);
380          if(FileExists(absolute))
381          {
382             prj = p;
383             break;
384          }
385       }
386       if(prj)
387          return CopyString(absolute);
388
389       strcpy(absolute, workspaceDir); //projectDir // CHECK?
390       PathCatSlash(absolute, relative);
391       if(FileExists(absolute))
392          return CopyString(absolute);
393
394       {
395          for(dir : sourceDirs)
396          {
397             strcpy(absolute, dir);
398             PathCatSlash(absolute, relative);
399             if(FileExists(absolute))
400                return CopyString(absolute);
401          }
402       }
403
404       return null;
405    }
406
407    char * CopyUnixPathWorkspaceRelativeOrAbsolute(const char * path)
408    {
409       if(IsPathInsideOf(path, workspaceDir))
410       {
411          char relativePath[MAX_LOCATION];
412          MakePathRelative(path, workspaceDir, relativePath);
413          return CopyUnixPath(relativePath);
414       }
415       else
416          return CopyUnixPath(path);
417    }
418
419    char * MakeRelativePath(char * buffer, const char * path)
420    {
421       char * result = null;
422       if(buffer && path)
423       {
424          MakePathRelative(path, workspaceDir, buffer);
425          MakeSlashPath(buffer);
426          result = buffer;
427       }
428       return result;
429    }
430
431    char * GetRelativePathForProject(char * buffer, Project project)
432    {
433       char * result = null;
434       if(buffer && project && project.topNode.path)
435       {
436          MakePathRelative(project.topNode.path, workspaceDir, buffer);
437          MakeSlashPath(buffer);
438          PathCatSlash(buffer, project.topNode.name);
439          result = buffer;
440       }
441       return result;
442    }
443
444    Array<ProjectNode> GetAllProjectNodes(const char *fullPath, bool skipExcluded)
445    {
446       Array<ProjectNode> nodes = null;
447       for(project : projects)
448       {
449          ProjectNode node;
450          if((node = project.topNode.FindByFullPath(fullPath, false)))
451          {
452             if(!skipExcluded || !node.GetIsExcluded(project.config))
453             {
454                if(!nodes) nodes = { };
455                nodes.Add(node);
456             }
457          }
458       }
459       return nodes;
460    }
461
462    Project GetFileOwner(const char * absolutePath, const char * objectFileExt)
463    {
464       Project owner = null;
465       for(prj : projects)
466       {
467          if(prj.topNode.FindByFullPath(absolutePath, false))
468          {
469             owner = prj;
470             break;
471          }
472       }
473       if(!owner)
474          GetObjectFileNode(absolutePath, &owner, null, objectFileExt);
475       return owner;
476    }
477
478    void GetRelativePath(const char * absolutePath, char * relativePath, Project * owner, const char * objectFileExt)
479    {
480       Project prj = GetFileOwner(absolutePath, objectFileExt);
481       if(owner)
482          *owner = prj;
483       if(!prj)
484          prj = projects.firstIterator.data;
485       if(prj)
486       {
487          MakePathRelative(absolutePath, prj.topNode.path, relativePath);
488          MakeSlashPath(relativePath);
489       }
490       else
491          relativePath[0] = '\0';
492    }
493
494    ProjectNode GetObjectFileNode(const char * filePath, Project * project, char * fullPath, const char * objectFileExt)
495    {
496       ProjectNode node = null;
497       char ext[MAX_EXTENSION];
498       GetExtension(filePath, ext);
499       if(ext[0])
500       {
501          IntermediateFileType type = IntermediateFileType::FromExtension(ext);
502          if(type)
503          {
504             char fileName[MAX_FILENAME];
505             GetLastDirectory(filePath, fileName);
506             if(fileName[0])
507             {
508                DotMain dotMain = DotMain::FromFileName(fileName);
509                for(prj : ide.workspace.projects)
510                {
511                   if((node = prj.FindNodeByObjectFileName(fileName, type, dotMain, null, objectFileExt)))
512                   {
513                      if(project)
514                         *project = prj;
515                      if(fullPath)
516                      {
517                         const char * cfgName = prj.config ? prj.config.name : "";
518                         char name[MAX_FILENAME];
519                         CompilerConfig compiler = ideSettings.GetCompilerConfig(prj.lastBuildCompilerName);
520                         DirExpression objDir = prj.GetObjDir(compiler, prj.config, bitDepth);
521                         strcpy(fullPath, prj.topNode.path);
522                         PathCatSlash(fullPath, objDir.dir);
523                         node.GetObjectFileName(name, prj.configsNameCollisions[cfgName], type, dotMain, objectFileExt);
524                         PathCatSlash(fullPath, name);
525                         delete objDir;
526                         delete compiler;
527                      }
528                      break;
529                   }
530                }
531             }
532          }
533       }
534       return node;
535    }
536
537    OpenedFileInfo UpdateOpenedFileInfo(const char * fileName, OpenedFileState state)
538    {
539       char absolutePath[MAX_LOCATION];
540       char relativePath[MAX_LOCATION];
541       OpenedFileInfo ofi;
542       GetSlashPathBuffer(absolutePath, fileName);
543       MakeRelativePath(relativePath, fileName);
544       ofi = FindOpenedFileInfo(relativePath, absolutePath);
545       if(state)
546       {
547          if(!ofi)
548          {
549             ofi = OpenedFileInfo { path = CopyString(relativePath) };
550             openedFiles.Add(ofi);
551          }
552          ofi.state = state;
553          ofi.modified = GetLocalTimeStamp();
554          if(!holdTracking)
555             modified = true;
556       }
557       else if(ofi)
558       {
559          Iterator<OpenedFileInfo> it { openedFiles };
560          if(it.Find(ofi))
561             openedFiles.Delete(it.pointer);
562          if(!holdTracking)
563             modified = true;
564       }
565       return ofi;
566    }
567
568    void LoadOpenedFileInfo(const char * path, OpenedFileState state, int lineNumber, int position, Point scroll, TimeStamp modified, Array<String> openedFilesNotFound)
569    {
570       char absolutePath[MAX_LOCATION];
571       char relativePath[MAX_LOCATION];
572       TimeStamp stamp = modified;
573       bool exists;
574       strcpy(absolutePath, workspaceDir);
575       PathCatSlash(absolutePath, path);
576       MakeRelativePath(relativePath, absolutePath);
577       if(!(exists = FileExists(absolutePath)))
578          stamp -= 60*60*24*20; // Days { 20 };
579       if(stamp > GetLocalTimeStamp() - 60*60*24*384) // Days { 384 });
580       {
581          if(state == closed || exists)
582          {
583             OpenedFileInfo ofi = FindOpenedFileInfo(relativePath, absolutePath);
584             if(!ofi)
585                openedFiles.Add(OpenedFileInfo { CopyString(relativePath), state, lineNumber, position, scroll, stamp });
586             // else silently drop duplicates if they should ever occur;
587          }
588          else
589             openedFilesNotFound.Add(CopyString(absolutePath));
590       }
591       // else silently discarding old or broken OpenedFileInfo entries;
592    }
593
594    void OpenPreviouslyOpenedFiles(bool noParsing)
595    {
596       Array<String> filePaths {};
597       holdTracking = true;
598       for(ofi : openedFiles; ofi.state != closed)
599          filePaths.Add(CopyString(ofi.path));
600       for(path : filePaths)
601       {
602          Window file;
603          char absolutePath[MAX_LOCATION];
604          strcpy(absolutePath, workspaceDir);
605          PathCatSlash(absolutePath, path);
606          file = ide.OpenFile(absolutePath, false, true, null, no, normal, noParsing);
607          if(file)
608          {
609             for(prj : projects)
610             {
611                ProjectNode node = prj.topNode.FindByFullPath(absolutePath, true);
612                if(node)
613                   node.EnsureVisible();
614             }
615          }
616       }
617       holdTracking = false;
618       filePaths.Free();
619       delete filePaths;
620    }
621
622    OpenedFileInfo FindOpenedFileInfo(const char * relativePath, const char * absolutePath)
623    {
624       OpenedFileInfo result = null;
625       for(e : openedFiles)
626       {
627          bool switchToRelative;
628          if((switchToRelative = !fstrcmp(absolutePath, e.path)) || !fstrcmp(relativePath, e.path))
629          {
630             result = e;
631             if(switchToRelative)
632             {
633                delete result.path;
634                result.path = CopyString(relativePath);
635             }
636             break;
637          }
638       }
639       return result;
640    }
641
642    void RestorePreviouslyOpenedFileState(CodeEditor editor)
643    {
644       if((editor.openedFileInfo = UpdateOpenedFileInfo(editor.fileName, opened)))
645          editor.openedFileInfo.SetCodeEditorState(editor);
646    }
647
648    void UpdateSourceDirsArray(Array<String> dirs)
649    {
650       sourceDirs.Free();
651
652       for(s : dirs)
653          sourceDirs.Add(CopyString(s));
654
655       DropInvalidBreakpoints(null);
656
657       delete dirs;
658    }
659
660    void RemoveProject(Project project)
661    {
662       Iterator<AddedProjectInfo> it { addedProjects };
663       while(it.Next())
664       {
665          if(it.data.project == project)
666          {
667             addedProjects.Remove(it.pointer);
668             break;
669          }
670       }
671       projects.TakeOut(project);
672
673       DropInvalidBreakpoints(project);
674       modified = true;
675       ide.findInFilesDialog.RemoveProjectItem(project);
676       ide.UpdateToolBarActiveConfigs(false);
677       Save();
678
679       delete project;
680    }
681
682    void SelectActiveConfig(const char * configName)
683    {
684       bool change = false;
685       for(prj : ide.workspace.projects)
686       {
687          for(cfg : prj.configurations)
688          {
689             if(cfg.name && !strcmp(cfg.name, configName))
690             {
691                prj.config = cfg;
692                change = true;
693                break;
694             }
695          }
696       }
697       if(change)
698       {
699          modified = true;
700          ide.UpdateToolBarActiveConfigs(true);
701          ide.projectView.Update(null);
702          Save();
703       }
704       ide.AdjustDebugMenus();
705    }
706
707    bool FindPath(ProjectNode node, const char * path)
708    {
709       if(node.type == file)
710       {
711          // TODO: Should this code be moved into a ProjectNode::absolutePath property? Taken from NodeProperties.ec
712          char filePath[MAX_LOCATION];
713          GetSlashPathBuffer(filePath, node.project.topNode.path);
714          PathCatSlash(filePath, node.path);
715          PathCatSlash(filePath, node.name);
716
717          if(!fstrcmp(filePath, path))
718             return true;
719       }
720       if(node.files)
721       {
722          for(n : node.files)
723          {
724             if(FindPath(n, path))
725                return true;
726          }
727       }
728       return false;
729    }
730
731    void ChangeBreakpoint(DataRow row, const char * location)
732    {
733       Breakpoint bp = (Breakpoint)(intptr)row.tag;
734       if(bp)
735       {
736          char * currentLoc = bp.CopyUserLocationString();
737          if(strcmp(location, currentLoc))
738          {
739             char * newLoc;
740             bp.location = CopyString(location);
741             bp.ParseLocation();
742             delete bp.location;
743             newLoc = bp.CopyUserLocationString();
744             if(strcmp(newLoc, currentLoc))
745             {
746                ide.breakpointsView.UpdateBreakpoint(row);
747                Save();
748             }
749          }
750          delete currentLoc;
751       }
752       else if(location)
753       {
754          // adding a breakpoint by typing it in the breakpoints view
755          // todo, parse location
756          //  if good, make add breakpoint, make sure possibly previously entered ignore and level are reflected in breakpoint
757          //  else
758          //     ...
759          //bp = Breakpoint { };
760          //row.tag = (int64)bp;
761          //breakpoints.Add(bp);
762          //bp.row = row;
763          Save();
764       }
765    }
766
767    void ChangeBreakpointIgnore(DataRow row, int ignore)
768    {
769       Breakpoint bp = (Breakpoint)(intptr)row.tag;
770       if(bp)
771       {
772          bp.ignore = ignore;
773          Save();
774       }
775    }
776
777    void ChangeBreakpointLevel(DataRow row, int level)
778    {
779       Breakpoint bp = (Breakpoint)(intptr)row.tag;
780       if(bp)
781       {
782          bp.level = level;
783          Save();
784       }
785    }
786
787    void ChangeBreakpointCondition(DataRow row, const char * condition)
788    {
789       Breakpoint bp = (Breakpoint)(intptr)row.tag;
790       if(bp && !(!bp.condition && !(condition && condition[0])))
791       {
792          if(!bp.condition)
793          {
794             bp.condition = Watch { };
795             bp.condition.expression = CopyString(condition);
796             Save();
797          }
798          else if(!(condition && condition[0]))
799          {
800             bp.condition.Free();
801             bp.condition = null;
802             Save();
803          }
804          else if(strcmp(condition, bp.condition.expression))
805          {
806             bp.condition.Free();
807             bp.condition = Watch { };
808             bp.condition.expression = CopyString(condition);
809             Save();
810          }
811       }
812    }
813
814    void RemoveBreakpoint(Breakpoint bp)
815    {
816       bpCount--;
817       ide.breakpointsView.RemoveBreakpoint(bp);
818       ide.debugger.UpdateRemovedBreakpoint(bp);
819       {
820          Iterator<Breakpoint> it { breakpoints };
821          if(it.Find(bp))
822             breakpoints.Remove(it.pointer);
823       }
824       {
825          Window document;
826          for(document = ide.firstChild; document; document = document.next)
827          {
828             const char * fileName = document.fileName;
829             if(document.isDocument && fileName && document.created)
830             {
831                char winFilePath[MAX_LOCATION];
832                const char * slashPath = GetSlashPathBuffer(winFilePath, fileName);
833
834                if(!fstrcmp(slashPath, bp.absoluteFilePath))
835                {
836                   CodeEditor codeEditor = (CodeEditor)document;
837                   int boxH = codeEditor.editBox.clientSize.h;
838                   Box box { 0, 0, 19, boxH - 1 };
839                   document.Update(box);
840                   break;
841                }
842             }
843          }
844       }
845       Save();
846       delete bp;
847    }
848
849    void ParseLoadedBreakpoints()
850    {
851       for(bp : breakpoints; bp.location)
852       {
853          bp.ParseLocation();
854          delete bp.location;
855          ide.breakpointsView.UpdateBreakpoint(bp.row);
856       }
857    }
858
859    void DropInvalidBreakpoints(Project removedProject) // think about not dropping BPs that are past end of file but simply disable them
860    {                                                   // why? when using a scm/vcs you might be alternating between two version of a file
861       if(breakpoints)                                  // and anoyingly keep loosing breakpoints in the version of the file you are working on
862       {
863          Link bpLink, next;
864          for(bpLink = breakpoints.first; bpLink; bpLink = next)
865          {
866             Breakpoint bp = (Breakpoint)(intptr)bpLink.data;
867             next = bpLink.next;
868             if(removedProject)
869             {
870                if(bp.project == removedProject)
871                {
872                   ide.breakpointsView.RemoveBreakpoint(bp);
873                   RemoveBreakpoint(bp);
874                }
875             }
876             else
877             {
878                Project project = bp.project;
879                if(!project)
880                {
881                   for(p : projects)
882                   {
883                      if(FindPath(p.topNode, bp.absoluteFilePath))
884                      {
885                         project = p;
886                         break;
887                      }
888                      // Handle symbol loader modules:
889                      {
890                         char moduleName[MAX_FILENAME];
891                         char * sl;
892                         GetLastDirectory(bp.absoluteFilePath, moduleName);
893                         // Tweak for automatically resolving symbol loader modules
894                         sl = strstr(moduleName, ".main.ec");
895                         if(sl && (*sl = 0, !strcmpi(moduleName, p.name)))
896                         {
897                            project = p;
898                            break;
899                         }
900                      }
901                   }
902                }
903                if(!project)
904                {
905                   bool found = false;
906                   for(dir : sourceDirs)
907                   {
908                      if(IsPathInsideOf(bp.absoluteFilePath, dir))
909                      {
910                         found = true;
911                         break;
912                      }
913                   }
914                   if(!found)
915                   {
916                      ide.breakpointsView.RemoveBreakpoint(bp);
917                      RemoveBreakpoint(bp);
918                   }
919                }
920             }
921          }
922          ide.breakpointsView.Update(null);
923       }
924    }
925
926    void Init()
927    {
928       if(!addedProjects) addedProjects = { };
929       if(!sourceDirs) sourceDirs = { };
930       if(!environmentVars) environmentVars = { };
931       if(!breakpoints) breakpoints = { };
932       if(!watches) watches = { };
933       if(!openedFiles) openedFiles = { };
934    }
935
936    void Free()
937    {
938       delete name;
939       delete workspaceFile;
940       delete workspaceDir;
941       delete commandLineArgs;
942       delete debugDir;
943       delete activeCompiler;
944
945       //project = null;
946
947       if(addedProjects) { addedProjects.Free(); delete addedProjects; }
948       if(sourceDirs) { sourceDirs.Free(); delete sourceDirs; }
949       if(environmentVars) { environmentVars.Free(); delete environmentVars; }
950       if(breakpoints) { breakpoints.Free(); delete breakpoints; }
951       if(watches) { watches.Free(); delete watches; }
952       if(openedFiles) { openedFiles.Free(); delete openedFiles; }
953
954       projects.Free();
955    }
956
957    Workspace()
958    {
959       ide.outputView.buildBox.Clear();
960       ide.outputView.debugBox.Clear();
961       ide.callStackView.Clear();
962       ide.watchesView.Clear();
963       ide.threadsView.Clear();
964       ide.breakpointsView.Clear();
965
966       property::debugDir = "";
967
968       SetSourceDirs(sourceDirs);
969    }
970
971    ~Workspace()
972    {
973       Save();
974       timer.Stop();
975
976       SetSourceDirs(null);
977       Free();
978    }
979
980 }
981
982 Workspace LoadWorkspace(const char * filePath, const char * fromProjectFile)
983 {
984    File f;
985    Workspace workspace = null;
986
987    f = FileOpen(filePath, read);
988    if(f)
989    {
990       Array<String> openedFilesNotFound { };
991       JSONResult result;
992       WorkspaceFile wf = null;
993       {
994          ECONParser parser { f = f };
995          f.Seek(0, start);
996          result = parser.GetObject(class(WorkspaceFile), &wf);
997          if(result != success)
998             delete wf;
999          delete parser;
1000       }
1001       if(!wf)
1002          workspace = LoadLegacyWorkspace(filePath, openedFilesNotFound);
1003       else if(wf.workspace)
1004       {
1005          workspace = wf.workspace;
1006          //incref workspace;
1007          workspace.workspaceFile = filePath;
1008          {
1009             char absolutePath[MAX_LOCATION];
1010             bool done = false;
1011             bool ahead = false;
1012             Iterator<AddedProjectInfo> it { workspace.addedProjects };
1013             while(!done && (ahead || !(done = !it.Next())))
1014             {
1015                // TODO: implement some type of time based pruning of "dea" added projects instead of just dropping them on the spot
1016                //         TimeStamp modified; modified = GetLocalTimeStamp();
1017                //               TimeStamp stamp = modified; if(stamp > GetLocalTimeStamp() - 60*60*24*20) // Days { 20 });
1018                AddedProjectInfo addedPrj = it.data;
1019                strcpy(absolutePath, workspace.workspaceDir);
1020                PathCatSlash(absolutePath, addedPrj.path);
1021                if(FileExists(absolutePath))
1022                {
1023                   Project loadedProject = LoadProject(absolutePath, null);
1024                   if(loadedProject)
1025                   {
1026                      workspace.AddProject(loadedProject, addedPrj);
1027                      loadedProject.StartMonitoring();
1028                   }
1029                   else if(workspace.projects.count == 0)
1030                   {
1031                      delete workspace;
1032                      break;
1033                   }
1034                   else
1035                   {
1036                      // TODO: (#524) show message or something when added project fails to load;
1037                   }
1038                   ahead = false;
1039                }
1040                else
1041                {
1042                   IteratorPointer notFound = it.pointer;
1043                   done = !it.Next();
1044                   workspace.addedProjects.Delete(notFound);
1045                   ahead = true;
1046                }
1047             }
1048             if(workspace)
1049             {
1050                for(bp : workspace.breakpoints)
1051                {
1052                   char * path = workspace.CopyAbsolutePathFromRelative(bp.relativeFilePath);
1053                   bp.type = user;
1054                   if(path)
1055                   {
1056                      bp.absoluteFilePath = path;
1057                      delete path;
1058                   }
1059                   else
1060                      bp.absoluteFilePath = "";
1061                }
1062                if(workspace.openedFiles && workspace.openedFiles.count)
1063                {
1064                   List<OpenedFileInfo> openedFiles = workspace.openedFiles;
1065                   workspace.openedFiles = { };
1066                   for(of : openedFiles)
1067                   {
1068                      workspace.LoadOpenedFileInfo(of.path, of.state, of.lineNumber, of.position, of.scroll, of.modified, openedFilesNotFound);
1069                   }
1070                   openedFiles.Free();
1071                   delete openedFiles;
1072                }
1073             }
1074          }
1075       }
1076
1077       if(workspace)
1078       {
1079          workspace.Init();
1080          if(!workspace.projects.first)
1081          {
1082             Project project;
1083             if(fromProjectFile)
1084                project = LoadProject(fromProjectFile /*projectFilePath*/, null);
1085             else
1086             {
1087                char projectFilePath[MAX_LOCATION];
1088                strcpy(projectFilePath, workspace.workspaceFile);
1089                ChangeExtension(projectFilePath, ProjectExtension, projectFilePath);
1090                project = LoadProject(projectFilePath, null);
1091             }
1092             if(project)
1093             {
1094                project.StartMonitoring();
1095                workspace.AddProject(project, null);
1096                workspace.name = CopyString(project.name);
1097             }
1098             else
1099             {
1100                MessageBox { type = ok, master = ide, contents = $"Workspace load file failed", text = $"Workspace Load File Error" }.Modal();
1101                delete workspace;
1102                return null;
1103             }
1104          }
1105
1106          if(openedFilesNotFound.count)
1107          {
1108             int c = 0;
1109             char s[2] = "";
1110             String files = new char[MAX_LOCATION * 16];
1111             char title[512];
1112             String msg = new char[MAX_LOCATION * 16 + 2048];
1113             strcpy(files,"\n");
1114
1115             if(openedFilesNotFound.count > 1)
1116                strcpy(s, "s");
1117
1118             for(item : openedFilesNotFound)
1119             {
1120                c++;
1121                if(c == 16)
1122                {
1123                   strcat(files, "\n...");
1124                   break;
1125                }
1126                strcat(files, "\n");
1127                strcat(files, item);
1128             }
1129
1130             sprintf(title, $"File%s not found", s);
1131             sprintf(msg, $"The following file%s could not be re-opened.%s", s, files);
1132
1133             MessageBox { type = ok, master = ide, contents = msg, text = title }.Modal();
1134
1135             delete files;
1136             delete msg;
1137          }
1138       }
1139       openedFilesNotFound.Free();
1140       delete openedFilesNotFound;
1141       delete wf;
1142       delete f;
1143    }
1144    else if(fromProjectFile)
1145    {
1146       //MessageBox { type = Ok, master = ide, contents = "Worspace load file failed", text = "Worspace Load File Error" }.Modal();
1147
1148       //char projectFile[MAX_LOCATION];
1149       Project newProject;
1150
1151       //strcpy(projectFile, filePath);
1152       //ChangeExtension(projectFile, ProjectExtension, projectFile);
1153       newProject = LoadProject(fromProjectFile /*projectFile*/, null);
1154
1155       if(newProject)
1156       {
1157          newProject.StartMonitoring();
1158          workspace = Workspace { property::workspaceFile = filePath };
1159
1160          workspace.Init();
1161          workspace.AddProject(newProject, null);
1162          workspace.Save();
1163       }
1164    }
1165
1166    if(workspace)
1167    {
1168       ide.ChangeFileDialogsDirectory(workspace.workspaceDir, false);
1169
1170       if(!workspace.activeCompiler || !workspace.activeCompiler[0])
1171          workspace.activeCompiler = defaultCompilerName;
1172    }
1173
1174    return workspace;
1175 }
1176
1177 Workspace LoadLegacyWorkspace(const char * filePath, Array<String> openedFilesNotFound)
1178 {
1179    File file;
1180    Workspace workspace = null;
1181
1182    file = FileOpen(filePath, read);
1183    if(file)
1184    {
1185       double version = 0;
1186       char section[128];
1187       char subSection[128];
1188
1189       workspace = Workspace { activeCompiler = ideSettings.defaultCompiler, property::workspaceFile = filePath };
1190       workspace.Init();
1191
1192       file.Seek(0, start);
1193       while(!file.Eof())
1194       {
1195          char buffer[65536];
1196          char * equal;
1197
1198          Watch wh;
1199          Breakpoint bp = null;
1200
1201          file.GetLine(buffer, 65536 - 1);
1202          TrimLSpaces(buffer, buffer);
1203          TrimRSpaces(buffer, buffer);
1204          if(strlen(buffer))
1205          {
1206             if(buffer[0] == '~')
1207             {
1208                equal = &buffer[0];
1209                equal[0] = ' ';
1210                TrimLSpaces(equal, equal);
1211                if(!strcmpi(section, "Debugger Data") && !strcmpi(subSection, "Watches"))
1212                {
1213                   wh = Watch { };
1214                   if(!workspace.watches) workspace.watches = { };
1215                   workspace.watches.Add(wh);
1216                   wh.expression = CopyString(equal);
1217                }
1218                else if(!strcmpi(section, "Debugger Data") && !strcmpi(subSection, "Breakpoints"))
1219                {
1220                   if(bp)
1221                   {
1222                      wh = Watch { };
1223                      wh.expression = CopyString(equal);
1224                      bp.condition = wh;
1225                   }
1226                }
1227                else if(!strcmpi(section, "Execution Data") && !strcmpi(subSection, "Environment Variables"))
1228                {
1229                   String value = strchr(equal, '=');
1230                   if(value)
1231                   {
1232                      *value = 0;
1233                      value++;
1234                      TrimRSpaces(equal, equal);
1235                      TrimLSpaces(value, value);
1236                      workspace.environmentVars.Add({ equal, value });
1237                   }
1238                }
1239             }
1240             else if(buffer[0] == '*')
1241             {
1242                equal = &buffer[0];
1243                equal[0] = ' ';
1244                TrimLSpaces(equal, equal);
1245                if(!strcmpi(section, "Debugger Data") && !strcmpi(subSection, "Breakpoints"))
1246                {
1247                   char * strEnabled = null;
1248                   char * strIgnore = null;
1249                   char * strLevel = null;
1250                   char * strLine = null;
1251                   char * strFile = null;
1252
1253                   strEnabled = equal;
1254                   if(strEnabled && strEnabled[0])
1255                   {
1256                      strIgnore = strstr(strEnabled, ",");
1257                      strIgnore[0] = '\0';
1258                      strIgnore++;
1259                   }
1260                   if(strIgnore && strIgnore[0])
1261                   {
1262                      strLevel = strstr(strIgnore, ",");
1263                      strLevel[0] = '\0';
1264                      strLevel++;
1265                   }
1266                   if(strLevel && strLevel[0])
1267                   {
1268                      strLine = strstr(strLevel, ",");
1269                      strLine[0] = '\0';
1270                      strLine++;
1271                   }
1272                   if(strLine && strLine[0])
1273                   {
1274                      strFile = strstr(strLine, ",");
1275                      strFile[0] = '\0';
1276                      strFile++;
1277                   }
1278                   if(strEnabled && strEnabled[0] && strIgnore && strIgnore[0] &&
1279                         strLevel && strLevel[0] && strLine && strLine[0] && strFile && strFile[0])
1280                   {
1281                      bool enabled;
1282                      int ignore;
1283                      int level;
1284                      int line;
1285
1286                      TrimLSpaces(strEnabled, strEnabled);
1287                      TrimRSpaces(strEnabled, strEnabled);
1288                      TrimLSpaces(strIgnore, strIgnore);
1289                      TrimRSpaces(strIgnore, strIgnore);
1290                      TrimLSpaces(strLevel, strLevel);
1291                      TrimRSpaces(strLevel, strLevel);
1292                      TrimLSpaces(strLevel, strLevel);
1293                      TrimRSpaces(strLevel, strLevel);
1294                      TrimLSpaces(strFile, strFile);
1295                      TrimRSpaces(strFile, strFile);
1296
1297                      enabled = (strEnabled[0] == '1');
1298                      ignore = atoi(strIgnore);
1299                      level = atoi(strLevel);
1300                      line = atoi(strLine);
1301
1302                      bp = { type = user, enabled = enabled, ignore = ignore, level = level, line = line };
1303                      if(!workspace.breakpoints)
1304                         workspace.breakpoints = { };
1305                      workspace.breakpoints.Add(bp);
1306                      bp.location = CopyString(strFile);
1307                   }
1308                }
1309             }
1310             else if(buffer[0] == '=' || buffer[0] == '-')
1311             {
1312                equal = &buffer[0];
1313                equal[0] = ' ';
1314                TrimLSpaces(equal, equal);
1315                if(!strcmpi(section, "Debugger Data") && !strcmpi(subSection, "Source Directories"))
1316                   workspace.sourceDirs.Add(CopyString(equal));
1317                else if(!strcmpi(section, "Opened Files"))
1318                {
1319                   OpenedFileState state = opened;
1320                   int lineNumber = 0;
1321                   int position = 0;
1322                   Point scroll { };
1323                   if(version == 0.01)
1324                   {
1325                      char * comma = strchr(equal, ',');
1326                      if(comma)
1327                      {
1328                         comma[0] = '\0';
1329                         lineNumber = atoi(equal);
1330                         equal = comma + 1;
1331                      }
1332                   }
1333                   else if(version >= 0.02)
1334                   {
1335                      char * column = strchr(equal, ':');
1336                      if(column)
1337                      {
1338                         column[0] = '\0';
1339                         if(strcmpi(equal, "O"))
1340                            state = closed;
1341                         column++;
1342                         equal = column;
1343                         column = strchr(equal, ':');
1344                         if(column)
1345                         {
1346                            column[0] = '\0';
1347                            lineNumber = atoi(equal);
1348                            column++;
1349                            equal = column;
1350                            column = strchr(equal, ':');
1351                            if(column)
1352                            {
1353                               column[0] = '\0';
1354                               position = atoi(equal);
1355                               column++;
1356                               equal = column;
1357                               column = strchr(equal, ':');
1358                               if(column)
1359                               {
1360                                  column[0] = '\0';
1361                                  scroll.x = atoi(equal);
1362                                  column++;
1363                                  equal = column;
1364                                  column = strchr(equal, ':');
1365                                  if(column)
1366                                  {
1367                                     column[0] = '\0';
1368                                     scroll.y = atoi(equal);
1369                                     column++;
1370                                     equal = column;
1371                                  }
1372                               }
1373                            }
1374                         }
1375                      }
1376                   }
1377                   workspace.LoadOpenedFileInfo(equal, state, lineNumber, position, scroll, GetLocalTimeStamp(), openedFilesNotFound);
1378                }
1379                else if(!strcmpi(section, "Projects"))
1380                {
1381                   char projectFilePath[MAX_LOCATION];
1382                   Project newProject;
1383                   strcpy(projectFilePath, workspace.workspaceDir);
1384                   PathCatSlash(projectFilePath, equal);
1385                   newProject = LoadProject(projectFilePath, null);
1386                   if(newProject)
1387                   {
1388                      workspace.AddProject(newProject, null);
1389                      newProject.StartMonitoring();
1390                   }
1391                   else if(workspace.projects.count == 0)
1392                   {
1393                      delete workspace;
1394                      break;
1395                   }
1396                   else
1397                   {
1398                      // TODO: (#524) show message or something when added project fails to load;
1399                   }
1400                }
1401             }
1402             else if(!strcmpi(buffer, "ECERE Workspace File"));
1403             else if(!strcmpi(buffer, "Version 0a"))
1404                version = 0;
1405             else if(!strncmp(buffer, "Version ", 8))
1406                version = atof(&buffer[8]);
1407             else if(!strcmpi(buffer, "Workspace"))
1408                strcpy(section, buffer);
1409             else if(!strcmpi(buffer, "Projects"))
1410                strcpy(section, buffer);
1411             else if(!strcmpi(buffer, "Execution Data"))
1412                strcpy(section, buffer);
1413             else if(!strcmpi(buffer, "Debugger Data"))
1414                strcpy(section, buffer);
1415             else if(!strcmpi(buffer, "Source Directories"))
1416                strcpy(subSection, buffer);
1417             else if(!strcmpi(buffer, "Breakpoints"))
1418                strcpy(subSection, buffer);
1419             else if(!strcmpi(buffer, "Watches"))
1420                strcpy(subSection, buffer);
1421             else if(!strcmpi(buffer, "Environment Variables"))
1422                strcpy(subSection, buffer);
1423             else if(!strcmpi(buffer, "Opened Files"))
1424             {
1425                strcpy(section, buffer);
1426                if(!workspace.openedFiles) workspace.openedFiles = { };
1427             }
1428             else if(!strcmpi(buffer, ""))      // | These two lines were commented out
1429                strcpy(subSection, buffer);     // | Do they serve a purpose? They were there for copy paste when adding a new subsection
1430             else
1431             {
1432                equal = strstr(buffer, "=");
1433                if(equal)
1434                {
1435                   if(!strcmpi(section, "Workspace"))
1436                   {
1437                      equal[0] = '\0';
1438                      TrimRSpaces(buffer, buffer);
1439                      equal++;
1440                      TrimLSpaces(equal, equal);
1441                      if(!strcmpi(buffer, "Active Compiler"))
1442                      {
1443                         CompilerConfig compiler = ideSettings.GetCompilerConfig(equal);
1444                         if(!compiler)
1445                            workspace.activeCompiler = defaultCompilerName;
1446                         else
1447                            workspace.activeCompiler = equal;
1448                         delete compiler;
1449                      }
1450                      if(!strcmpi(buffer, "Active Bit Depth"))
1451                      {
1452                         int bitDepth = atoi(equal);
1453                         if(!(bitDepth == 32 || bitDepth == 64))
1454                            bitDepth = 0;
1455                         workspace.bitDepth = bitDepth;
1456                         ide.toolBar.activeBitDepth.SelectRow(ide.toolBar.activeBitDepth.FindRow(bitDepth));
1457                      }
1458                   }
1459                   else if(!strcmpi(section, "Execution Data"))
1460                   {
1461                      equal[0] = '\0';
1462                      TrimRSpaces(buffer, buffer);
1463                      equal++;
1464                      TrimLSpaces(equal, equal);
1465                      if(!strcmpi(buffer, "Command Line Arguments"))
1466                         workspace.commandLineArgs = equal;
1467
1468                      if(!strcmpi(buffer, "Environment Variables"))
1469                      {
1470                         workspace.environmentVars.Free();
1471                         delete workspace.environmentVars;
1472                         workspace.environmentVars = { };
1473                      }
1474
1475                   }
1476                   else if(!strcmpi(section, "Debugger Data"))
1477                   {
1478                      equal[0] = '\0';
1479                      TrimRSpaces(buffer, buffer);
1480                      equal++;
1481                      TrimLSpaces(equal, equal);
1482                      if(!strcmpi(buffer, "Debug Working Directory"))
1483                         workspace.debugDir = equal;
1484                   }
1485                   else
1486                   {
1487                      equal[0] = '\0';
1488                      TrimRSpaces(buffer, buffer);
1489                      equal++;
1490                      TrimLSpaces(equal, equal);
1491                      if(!strcmpi(buffer, "Active Configuration"))
1492                      {
1493                         Project prj;
1494                         if(workspace.projects.last)
1495                         {
1496                            prj = workspace.projects.lastIterator.data;
1497                            for(cfg : prj.configurations)
1498                            {
1499                               if(!strcmp(cfg.name, equal))
1500                               {
1501                                  prj.config = cfg;
1502                                  break;
1503                               }
1504                            }
1505                         }
1506                      }
1507                      else if(!strcmpi(buffer, "Modified Compiler Config") || !strcmpi(buffer, "Modified Linker Config"))
1508                      {
1509                         Project prj;
1510                         if(workspace.projects.last)
1511                         {
1512                            prj = workspace.projects.lastIterator.data;
1513                            for(cfg : prj.configurations)
1514                            {
1515                               if(!strcmp(cfg.name, equal))
1516                               {
1517                                  if(strstr(buffer, "Compiler"))
1518                                     cfg.compilingModified = true;
1519                                  else
1520                                     cfg.linkingModified = true;
1521                                  break;
1522                               }
1523                            }
1524                         }
1525                      }
1526                      else if(!strcmpi(buffer, "CommandLineArgs"))
1527                         workspace.commandLineArgs = equal;
1528                      else if(!strcmpi(buffer, "Breakpoint"))
1529                      {
1530                         bool enabled;
1531                         char * lineNum = strstr(equal, ",");
1532                         if(lineNum)
1533                         {
1534                            lineNum[0] = '\0';
1535                            lineNum++;
1536                            if(equal[0] == '0')
1537                               enabled = false;
1538                            else
1539                               enabled = true;
1540                            if(lineNum)
1541                            {
1542                               char * absPath = strstr(lineNum, ",");
1543                               if(absPath)
1544                               {
1545                                  absPath[0] = '\0';
1546                                  absPath++;
1547                                  if(absPath)
1548                                  {
1549                                     char * relPath = strstr(absPath, ",");
1550                                     if(relPath)
1551                                     {
1552                                        relPath[0] = '\0';
1553                                        relPath++;
1554                                        if(relPath)
1555                                        {
1556                                           bp = { type = user, enabled = enabled, level = -1 };
1557                                           workspace.breakpoints.Add(bp);
1558                                           bp.line = atoi(lineNum);
1559                                           bp.location = relPath;
1560                                        }
1561                                     }
1562                                  }
1563                               }
1564                            }
1565                         }
1566                      }
1567                      else if(!strcmpi(buffer, "Watch"))
1568                      {
1569                         wh = Watch { };
1570                         workspace.watches.Add(wh);
1571                         wh.expression = CopyString(equal);
1572                      }
1573                      else if(!strcmpi(buffer, "SourceDir"))
1574                      {
1575                         workspace.sourceDirs.Add(CopyString(equal));
1576                      }
1577                      else if(!strcmpi(buffer, "DebugDir"))
1578                      {
1579                         workspace.debugDir = equal;
1580                      }
1581                   }
1582                }
1583             }
1584          }
1585       }
1586       delete file;
1587    }
1588    return workspace;
1589 }
1590
1591 TimeStamp GetLocalTimeStamp()
1592 {
1593    TimeStamp now;
1594    DateTime time { };
1595    time.GetLocalTime();
1596    now = time;
1597    return now;
1598 }