1 module dlangide.workspace.project;
2 
3 import dlangide.workspace.workspace;
4 import dlangide.workspace.projectsettings;
5 import dlangui.core.logger;
6 import dlangui.core.collections;
7 import dlangui.core.settings;
8 import std.algorithm;
9 import std.array : empty;
10 import std.file;
11 import std.path;
12 import std.process;
13 import std.utf;
14 import std.ascii : isAlphaNum;
15 
16 string[] includePath;
17 
18 /// return true if filename matches rules for workspace file names
19 bool isProjectFile(in string filename) pure nothrow {
20     return filename.baseName.equal("dub.json") || filename.baseName.equal("DUB.JSON") || filename.baseName.equal("package.json") ||
21         filename.baseName.equal("dub.sdl") || filename.baseName.equal("DUB.SDL");
22 }
23 
24 string toForwardSlashSeparator(in string filename) pure nothrow {
25     char[] res;
26     foreach(ch; filename) {
27         if (ch == '\\')
28             res ~= '/';
29         else
30             res ~= ch;
31     }
32     return cast(string)res;
33 }
34 
35 /// project item
36 class ProjectItem {
37     protected Project _project;
38     protected ProjectItem _parent;
39     protected string _filename;
40     protected dstring _name;
41 
42     this(string filename) {
43         _filename = buildNormalizedPath(filename);
44         _name = toUTF32(baseName(_filename));
45     }
46 
47     this() {
48     }
49 
50     @property ProjectItem parent() { return _parent; }
51 
52     @property Project project() { return _project; }
53 
54     @property void project(Project p) { _project = p; }
55 
56     @property string filename() { return _filename; }
57 
58     @property string directory() {
59         import std.path : dirName;
60         return _filename.dirName;
61     }
62 
63     @property dstring name() { return _name; }
64 
65     @property string name8() {
66         return _name.toUTF8;
67     }
68 
69     /// returns true if item is folder
70     @property const bool isFolder() { return false; }
71     /// returns child object count
72     @property int childCount() { return 0; }
73     /// returns child item by index
74     ProjectItem child(int index) { return null; }
75 
76     void refresh() {
77     }
78 
79     ProjectSourceFile findSourceFile(string projectFileName, string fullFileName) {
80         if (fullFileName.equal(_filename))
81             return cast(ProjectSourceFile)this;
82         if (project && projectFileName.equal(project.absoluteToRelativePath(_filename)))
83             return cast(ProjectSourceFile)this;
84         return null;
85     }
86 
87     @property bool isDSourceFile() {
88         if (isFolder)
89             return false;
90         return filename.endsWith(".d") || filename.endsWith(".dd") || filename.endsWith(".dd")  || filename.endsWith(".di") || filename.endsWith(".dh") || filename.endsWith(".ddoc");
91     }
92 
93     @property bool isJsonFile() {
94         if (isFolder)
95             return false;
96         return filename.endsWith(".json") || filename.endsWith(".JSON");
97     }
98 
99     @property bool isDMLFile() {
100         if (isFolder)
101             return false;
102         return filename.endsWith(".dml") || filename.endsWith(".DML");
103     }
104 
105     @property bool isXMLFile() {
106         if (isFolder)
107             return false;
108         return filename.endsWith(".xml") || filename.endsWith(".XML");
109     }
110 }
111 
112 /// Project folder
113 class ProjectFolder : ProjectItem {
114     protected ObjectList!ProjectItem _children;
115 
116     this(string filename) {
117         super(filename);
118     }
119 
120     @property override const bool isFolder() {
121         return true;
122     }
123     @property override int childCount() {
124         return _children.count;
125     }
126     /// returns child item by index
127     override ProjectItem child(int index) {
128         return _children[index];
129     }
130     void addChild(ProjectItem item) {
131         _children.add(item);
132         item._parent = this;
133         item._project = _project;
134     }
135     ProjectItem childByPathName(string path) {
136         for (int i = 0; i < _children.count; i++) {
137             if (_children[i].filename.equal(path))
138                 return _children[i];
139         }
140         return null;
141     }
142     ProjectItem childByName(dstring s) {
143         for (int i = 0; i < _children.count; i++) {
144             if (_children[i].name.equal(s))
145                 return _children[i];
146         }
147         return null;
148     }
149 
150     override ProjectSourceFile findSourceFile(string projectFileName, string fullFileName) {
151         for (int i = 0; i < _children.count; i++) {
152             if (ProjectSourceFile res = _children[i].findSourceFile(projectFileName, fullFileName))
153                 return res;
154         }
155         return null;
156     }
157 
158     bool loadDir(string path) {
159         string src = relativeToAbsolutePath(path);
160         if (exists(src) && isDir(src)) {
161             ProjectFolder existing = cast(ProjectFolder)childByPathName(src);
162             if (existing) {
163                 if (existing.isFolder)
164                     existing.loadItems();
165                 return true;
166             }
167             auto dir = new ProjectFolder(src);
168             addChild(dir);
169             Log.d("    added project folder ", src);
170             dir.loadItems();
171             return true;
172         }
173         return false;
174     }
175 
176     bool loadFile(string path) {
177         string src = relativeToAbsolutePath(path);
178         if (exists(src) && isFile(src)) {
179             ProjectItem existing = childByPathName(src);
180             if (existing)
181                 return true;
182             auto f = new ProjectSourceFile(src);
183             addChild(f);
184             Log.d("    added project file ", src);
185             return true;
186         }
187         return false;
188     }
189 
190     void loadItems() {
191         bool[string] loaded;
192         string path = _filename;
193         if (exists(path) && isFile(path))
194             path = dirName(path);
195         foreach(e; dirEntries(path, SpanMode.shallow)) {
196             string fn = baseName(e.name);
197             if (e.isDir) {
198                 loadDir(fn);
199                 loaded[fn] = true;
200             } else if (e.isFile) {
201                 loadFile(fn);
202                 loaded[fn] = true;
203             }
204         }
205         // removing non-reloaded items
206         for (int i = _children.count - 1; i >= 0; i--) {
207             if (!(toUTF8(_children[i].name) in loaded)) {
208                 _children.remove(i);
209             }
210         }
211         sortItems();
212     }
213 
214     /// predicate for sorting project items
215     static bool compareProjectItemsLess(ProjectItem item1, ProjectItem item2) {
216         return ((item1.isFolder && !item2.isFolder) || ((item1.isFolder == item2.isFolder) && (item1.name < item2.name)));
217     }
218 
219     void sortItems() {
220         import std.algorithm.sorting : sort;
221         sort!compareProjectItemsLess(_children.asArray);
222     }
223 
224     string relativeToAbsolutePath(string path) {
225         if (isAbsolute(path))
226             return path;
227         string fn = _filename;
228         if (exists(fn) && isFile(fn))
229             fn = dirName(fn);
230         return buildNormalizedPath(fn, path);
231     }
232 
233     override void refresh() {
234         loadItems();
235     }
236 }
237 
238 
239 /// Project source file
240 class ProjectSourceFile : ProjectItem {
241     this(string filename) {
242         super(filename);
243     }
244     /// file path relative to project directory
245     @property string projectFilePath() {
246         return project.absoluteToRelativePath(filename);
247     }
248 
249     void setFilename(string filename) {
250         _filename = buildNormalizedPath(filename);
251         _name = toUTF32(baseName(_filename));
252     }
253 }
254 
255 class WorkspaceItem {
256     protected string _filename;
257     protected string _dir;
258     protected dstring _name;
259     protected dstring _originalName;
260     protected dstring _description;
261 
262     this(string fname = null) {
263         filename = fname;
264     }
265 
266     /// file name of workspace item
267     @property string filename() { return _filename; }
268 
269     /// workspace item directory
270     @property string dir() { return _dir; }
271 
272     /// file name of workspace item
273     @property void filename(string fname) {
274         if (fname.length > 0) {
275             _filename = buildNormalizedPath(fname);
276             _dir = dirName(filename);
277         } else {
278             _filename = null;
279             _dir = null;
280         }
281     }
282 
283     /// name
284     @property dstring name() { return _name; }
285 
286     @property string name8() {
287         return _name.toUTF8;
288     }
289 
290     /// name
291     @property void name(dstring s) {  _name = s; }
292 
293     /// description
294     @property dstring description() { return _description; }
295     /// description
296     @property void description(dstring s) { _description = s; }
297 
298     /// load
299     bool load(string fname) {
300         // override it
301         return false;
302     }
303 
304     bool save(string fname = null) {
305         return false;
306     }
307 }
308 
309 /// Stores info about project configuration
310 struct ProjectConfiguration {
311     /// name used to build the project
312     string name;
313     /// type, for libraries one can run tests, for apps - execute them
314     Type type;
315     
316     /// How to display default configuration in ui
317     immutable static string DEFAULT_NAME = "default";
318     /// Default project configuration
319     immutable static ProjectConfiguration DEFAULT = ProjectConfiguration(DEFAULT_NAME, Type.Default);
320     
321     /// Type of configuration
322     enum Type {
323         Default,
324         Executable,
325         Library
326     }
327     
328     private static Type parseType(string s)
329     {
330         switch(s)
331         {
332             case "executable": return Type.Executable;
333             case "library": return Type.Library;
334             case "dynamicLibrary": return Type.Library;
335             case "staticLibrary": return Type.Library;
336             default: return Type.Default;
337         }
338     }
339     
340     /// parsing from setting file
341     static ProjectConfiguration[] load(Setting s)
342     {
343         ProjectConfiguration[] res;
344         Setting configs = s.objectByPath("configurations");
345         if(configs is null || configs.type != SettingType.ARRAY) {
346             res ~= DEFAULT;
347             return res;
348         }
349 
350         foreach(conf; configs) {
351             if(!conf.isObject) continue;
352             Type t = Type.Default;
353             if(auto typeName = conf.getString("targetType"))
354                 t = parseType(typeName);
355             if (string confName = conf.getString("name")) {
356                 res ~= ProjectConfiguration(confName, t);
357             }
358         }
359         return res;
360     }
361 }
362 
363 /// DLANGIDE D project
364 class Project : WorkspaceItem {
365     import dlangide.workspace.idesettings : IDESettings;
366     protected Workspace _workspace;
367     protected bool _opened;
368     protected ProjectFolder _items;
369     protected ProjectSourceFile _mainSourceFile;
370     protected SettingsFile _projectFile;
371     protected ProjectSettings _settingsFile;
372     protected bool _isDependency;
373     protected bool _isSubproject;
374     protected bool _isEmbeddedSubproject;
375     protected dstring _baseProjectName;
376     protected string _dependencyVersion;
377 
378     protected string[] _sourcePaths;
379     protected ProjectConfiguration[] _configurations;
380     protected ProjectConfiguration _projectConfiguration = ProjectConfiguration.DEFAULT;
381 
382     @property int projectConfigurationIndex() {
383         ProjectConfiguration config = projectConfiguration();
384         foreach(i, value; _configurations) {
385             if (value.name == config.name)
386                 return cast(int)i;
387         }
388         return 0;
389     }
390 
391     @property ProjectConfiguration projectConfiguration() {
392         if (_configurations.length == 0)
393             return ProjectConfiguration.DEFAULT;
394         string configName = settings.projectConfiguration;
395         foreach(config; _configurations) {
396             if (configName == config.name)
397                 return config;
398         }
399         return _configurations[0];
400     }
401 
402     @property void projectConfiguration(ProjectConfiguration config) {
403         settings.projectConfiguration = config.name;
404         settings.save();
405     }
406 
407     @property void projectConfiguration(string configName) {
408         foreach(name, config; _configurations) {
409             if (configName == config.name) {
410                 settings.projectConfiguration = config.name;
411                 settings.save();
412                 return;
413             }
414         }
415     }
416 
417     this(Workspace ws, string fname = null, string dependencyVersion = null) {
418         super(fname);
419         _workspace = ws;
420 
421         if (_workspace) {
422     		foreach(obj; _workspace.includePath.array)
423     			includePath ~= obj.str;
424         }
425 
426         _items = new ProjectFolder(fname.dirName);
427         _dependencyVersion = dependencyVersion;
428         _isDependency = _dependencyVersion.length > 0;
429         _projectFile = new SettingsFile(fname);
430     }
431 
432     void setBaseProject(Project p) {
433         if (p) {
434             _isSubproject = true;
435             _isDependency = p._isDependency;
436             _baseProjectName = p._originalName;
437             _dependencyVersion = p._dependencyVersion;
438         } else {
439             _isSubproject = false;
440         }
441     }
442 
443     void setSubprojectJson(Setting s) {
444         if (!_projectFile)
445             _projectFile = new SettingsFile();
446         _isEmbeddedSubproject = true;
447         _projectFile.replaceSetting(s.clone);
448     }
449 
450     @property ProjectSettings settings() {
451         if (!_settingsFile) {
452             _settingsFile = new ProjectSettings(settingsFileName);
453             _settingsFile.updateDefaults();
454             _settingsFile.load();
455             _settingsFile.save();
456         }
457         return _settingsFile;
458     }
459 
460     @property string settingsFileName() {
461         return buildNormalizedPath(dir, toUTF8(name) ~ ".settings");
462     }
463 
464     @property bool isDependency() { return _isDependency; }
465     @property string dependencyVersion() { return _dependencyVersion; }
466 
467     /// returns project configurations
468     @property const(ProjectConfiguration[]) configurations() const
469     {
470         return _configurations;
471     }
472 
473     /// returns project configurations
474     @property dstring[] configurationNames() const
475     {
476         dstring[] res;
477         res.assumeSafeAppend;
478         foreach(conf; _configurations)
479             res ~= conf.name.toUTF32;
480         return res;
481     }
482 
483     /// direct access to project file (json)
484     @property SettingsFile content() { return _projectFile; }
485 
486     /// name
487     override @property dstring name() {
488         return super.name();
489     }
490 
491     /// name
492     override @property void name(dstring s) {
493         super.name(s);
494         _projectFile.setString("name", toUTF8(s));
495     }
496 
497     /// name
498     override @property dstring description() {
499         return super.description();
500     }
501 
502     /// name
503     override @property void description(dstring s) {
504         super.description(s);
505         _projectFile.setString("description", toUTF8(s));
506     }
507 
508     /// returns project's own source paths
509     @property string[] sourcePaths() { return _sourcePaths; }
510     /// returns project's current toolchain import paths
511     string[] builderSourcePaths(IDESettings ideSettings) {
512         string compilerName = settings.getToolchain(ideSettings);
513         if (!compilerName)
514             compilerName = "default";
515         return compilerImportPathsCache.getImportPathsFor(compilerName);
516     }
517 
518     /// returns first source folder for project or null if not found
519     ProjectFolder firstSourceFolder() {
520         for(int i = 0; i < _items.childCount; i++) {
521             if (_items.child(i).isFolder)
522                 return cast(ProjectFolder)_items.child(i);
523         }
524         return null;
525     }
526 
527     ProjectSourceFile findSourceFile(string projectFileName, string fullFileName) {
528         return _items ? _items.findSourceFile(projectFileName, fullFileName) : null;
529     }
530 
531     private static void addUnique(ref string[] dst, string[] items) {
532         foreach(item; items) {
533             if (!canFind(dst, item))
534                 dst ~= item;
535         }
536     }
537     @property string[] importPaths(IDESettings ideSettings) {
538         string[] res;
539         addUnique(res, sourcePaths);
540         addUnique(res, builderSourcePaths(ideSettings));
541         foreach(dep; _dependencies) {
542             addUnique(res, dep.sourcePaths);
543         }
544         return res;
545     }
546 
547     string relativeToAbsolutePath(string path) {
548         if (isAbsolute(path))
549             return path;
550         return buildNormalizedPath(_dir, path);
551     }
552 
553     string absoluteToRelativePath(string path) {
554         if (!isAbsolute(path))
555             return path;
556         return relativePath(path, _dir);
557     }
558 
559     @property ProjectSourceFile mainSourceFile() { return _mainSourceFile; }
560     @property ProjectFolder items() { return _items; }
561 
562     @property Workspace workspace() { return _workspace; }
563 
564     @property void workspace(Workspace p) { _workspace = p; }
565 
566     @property string defWorkspaceFile() {
567         return buildNormalizedPath(_filename.dirName, toUTF8(name) ~ WORKSPACE_EXTENSION);
568     }
569 
570     @property bool isExecutable() {
571         // TODO: use targetType
572         return true;
573     }
574 
575     /// return executable file name, or null if it's library project or executable is not found
576     @property string executableFileName() {
577         if (!isExecutable)
578             return null;
579         string exename = toUTF8(name);
580         exename = _projectFile.getString("targetName", exename);
581         // TODO: use targetName
582         version (Windows) {
583             exename = exename ~ ".exe";
584         }
585         string targetPath = _projectFile.getString("targetPath", null);
586         string exePath;
587         if (targetPath.length)
588             exePath = buildNormalizedPath(_filename.dirName, targetPath, exename); // int $targetPath directory
589         else
590             exePath = buildNormalizedPath(_filename.dirName, exename); // in project directory
591         return exePath;
592     }
593 
594     /// working directory for running and debugging project
595     @property string workingDirectory() {
596         // TODO: get from settings
597         return _filename.dirName;
598     }
599 
600     /// commandline parameters for running and debugging project
601     @property string runArgs() {
602         // TODO: get from settings
603         return null;
604     }
605 
606     @property bool runInExternalConsole() {
607         // TODO
608         return settings.runInExternalConsole;
609     }
610 
611     private ProjectFolder findItems(string[] srcPaths) {
612         auto folder = new ProjectFolder(_filename.dirName);
613         folder.project = this;
614         foreach(customPath; srcPaths) {
615             string path = relativeToAbsolutePath(customPath);
616             if (folder.loadDir(path))
617                 _sourcePaths ~= path;
618         }
619         return folder;
620     }
621 
622     void refresh() {
623         for (int i = _items._children.count - 1; i >= 0; i--) {
624             if (_items._children[i].isFolder)
625                 _items._children[i].refresh();
626         }
627     }
628 
629     void findMainSourceFile() {
630         string n = toUTF8(name);
631         string[] mainnames = ["app.d", "main.d", n ~ ".d"];
632         foreach(sname; mainnames) {
633             _mainSourceFile = findSourceFileItem(buildNormalizedPath(_dir, "src", sname));
634             if (_mainSourceFile)
635                 break;
636             _mainSourceFile = findSourceFileItem(buildNormalizedPath(_dir, "source", sname));
637             if (_mainSourceFile)
638                 break;
639         }
640     }
641 
642     /// tries to find source file in project, returns found project source file item, or null if not found
643     ProjectSourceFile findSourceFileItem(ProjectItem dir, string filename, bool fullFileName=true) {
644         foreach(i; 0 .. dir.childCount) {
645             ProjectItem item = dir.child(i);
646             if (item.isFolder) {
647                 ProjectSourceFile res = findSourceFileItem(item, filename, fullFileName);
648                 if (res)
649                     return res;
650             } else {
651                 auto res = cast(ProjectSourceFile)item;
652                 if(res)
653                 {
654                     if(fullFileName && res.filename.equal(filename))
655                         return res;
656                     else if (!fullFileName && res.filename.endsWith(filename))
657                         return res;
658                 }
659             }
660         }
661         return null;
662     }
663 
664     ProjectSourceFile findSourceFileItem(string filename, bool fullFileName=true) {
665         return findSourceFileItem(_items, filename, fullFileName);
666     }
667 
668     protected Project[] _subPackages;
669 
670     /// add item to string array ignoring duplicates
671     protected static void addUnique(ref string[] list, string item) {
672         foreach(s; list)
673             if (s == item)
674                 return;
675         list ~= item;
676     }
677 
678     /// add item to string array ignoring duplicates
679     protected void addRelativePathIfExists(ref string[] list, string item) {
680         item = relativeToAbsolutePath(item);
681         if (item.exists && item.isDir)
682             addUnique(list, item);
683     }
684 
685     /// find source paths for project
686     protected string[] findSourcePaths() {
687         string[] res;
688         res.assumeSafeAppend;
689         string[] srcPaths = _projectFile.getStringArray("sourcePaths");
690         foreach(s; srcPaths)
691             addRelativePathIfExists(res, s);
692         Setting configs = _projectFile.objectByPath("configurations");
693         if (configs) {
694             for (int i = 0; i < configs.length; i++) {
695                 Setting s = configs[i];
696                 if (s) {
697                     string[] paths = s.getStringArray("sourcePaths");
698                     foreach(path; paths)
699                         addRelativePathIfExists(res, path);
700                 }
701             }
702         }
703         if (!res.length) {
704             addRelativePathIfExists(res, "src");
705             addRelativePathIfExists(res, "source");
706         }
707 
708         return res;
709     }
710 
711     void processSubpackages() {
712         import dlangui.core.files;
713         _subPackages.length = 0;
714         Setting subPackages = _projectFile.settingByPath("subPackages", SettingType.ARRAY, false);
715         if (subPackages) {
716             string p = _projectFile.filename.dirName;
717             for(int i = 0; i < subPackages.length; i++) {
718                 Setting sp = subPackages[i];
719                 if (sp.isString) {
720                     // string
721                     string path = convertPathDelimiters(sp.str);
722                     string relative = relativePath(path, p);
723                     path = buildNormalizedPath(absolutePath(relative, p));
724                     //Log.d("Subproject path: ", path);
725                     string fn = DubPackageFinder.findPackageFile(path);
726                     //Log.d("Subproject file: ", fn);
727                     Project prj = new Project(_workspace, fn);
728                     prj.setBaseProject(this);
729                     if (prj.load()) {
730                         Log.d("Loaded subpackage from file: ", fn);
731                         _subPackages ~= prj;
732                         if (_workspace)
733                             _workspace.addDependencyProject(prj);
734                     } else {
735                         Log.w("Failed to load subpackage from file: ", fn);
736                     }
737                 } else if (sp.isObject) {
738                     // object - file inside base project dub.json
739                     Log.d("Subpackage is JSON object");
740                     string subname = sp.getString("name");
741                     if (subname) {
742                         Project prj = new Project(_workspace, _filename ~ "@" ~ subname);
743                         prj.setBaseProject(this);
744                         prj.setSubprojectJson(sp);
745                         bool res = prj.processLoadedProject();
746                         if (res) {
747                             Log.d("Added embedded subpackage ", subname);
748                             _subPackages ~= prj;
749                             if (_workspace)
750                                 _workspace.addDependencyProject(prj);
751                         } else {
752                             Log.w("Error while processing embedded subpackage");
753                         }
754                     }
755                 }
756             }
757         }
758     }
759 
760     /// parse data from _projectFile after loading
761     bool processLoadedProject() {
762         //
763         _mainSourceFile = null;
764         try {
765             _name = toUTF32(_projectFile.getString("name"));
766             _originalName = _name;
767             if (_baseProjectName) {
768                 _name = _baseProjectName ~ ":" ~ _name;
769             }
770             if (_isDependency) {
771                 _name ~= "-"d;
772                 _name ~= toUTF32(_dependencyVersion.startsWith("~") ? _dependencyVersion[1..$] : _dependencyVersion);
773             }
774             _description = toUTF32(_projectFile.getString("description"));
775             Log.d("  project name: ", _name);
776             Log.d("  project description: ", _description);
777 
778             processSubpackages();
779 
780             string[] srcPaths = findSourcePaths();
781             _sourcePaths = null;
782             _items = findItems(srcPaths);
783             findMainSourceFile();
784 
785             Log.i("Project source paths: ", sourcePaths);
786             //Log.i("Builder source paths: ", builderSourcePaths(_settings));
787             if (!_isDependency)
788                 loadSelections();
789 
790             _configurations = ProjectConfiguration.load(_projectFile);
791             Log.i("Project configurations: ", _configurations);
792 
793 
794         } catch (Exception e) {
795             Log.e("Cannot read project file", e);
796             return false;
797         }
798         _items.loadFile(filename);
799         return true;
800     }
801 
802     override bool load(string fname = null) {
803         if (!_projectFile)
804             _projectFile = new SettingsFile();
805         if (fname.length > 0)
806             filename = fname;
807         if (!_projectFile.load(_filename)) {
808             Log.e("failed to load project from file ", _filename);
809             return false;
810         }
811         Log.d("Reading project from file ", _filename);
812         return processLoadedProject();
813 
814     }
815 
816     override bool save(string fname = null) {
817         if (_isEmbeddedSubproject)
818             return false;
819         if (fname !is null)
820             filename = fname;
821         assert(filename !is null);
822         return _projectFile.save(filename, true);
823     }
824 
825     protected Project[] _dependencies;
826     @property Project[] dependencies() { return _dependencies; }
827 
828     Project findDependencyProject(string filename) {
829         foreach(dep; _dependencies) {
830             if (dep.filename.equal(filename))
831                 return dep;
832         }
833         return null;
834     }
835 
836     bool loadSelections() {
837         Project[] newdeps;
838         _dependencies.length = 0;
839         auto finder = new DubPackageFinder;
840         scope(exit) destroy(finder);
841         SettingsFile selectionsFile = new SettingsFile(buildNormalizedPath(_dir, "dub.selections.json"));
842         if (!selectionsFile.load()) {
843             _dependencies = newdeps;
844             return false;
845         }
846         Setting versions = selectionsFile.objectByPath("versions");
847         if (!versions.isObject) {
848             _dependencies = newdeps;
849             return false;
850         }
851         string[string] versionMap = versions.strMap;
852         foreach(packageName, packageVersion; versionMap) {
853             string fn = finder.findPackage(packageName, packageVersion);
854             Log.d("dependency ", packageName, " ", packageVersion, " : ", fn ? fn : "NOT FOUND");
855             if (fn) {
856                 Project p = findDependencyProject(fn);
857                 if (p) {
858                     Log.d("Found existing dependency project ", fn);
859                     newdeps ~= p;
860                     continue;
861                 }
862                 p = new Project(_workspace, fn, packageVersion);
863                 if (p.load()) {
864                     newdeps ~= p;
865                     if (_workspace)
866                         _workspace.addDependencyProject(p);
867                 } else {
868                     Log.e("cannot load dependency package ", packageName, " ", packageVersion, " from file ", fn);
869                     destroy(p);
870                 }
871             }
872         }
873         _dependencies = newdeps;
874         return true;
875     }
876 }
877 
878 class DubPackageFinder {
879     string systemDubPath;
880     string userDubPath;
881     string tempPath;
882     this() {
883         version(Windows){
884             systemDubPath = buildNormalizedPath(environment.get("ProgramData"), "dub", "packages");
885             userDubPath = buildNormalizedPath(environment.get("APPDATA"), "dub", "packages");
886             tempPath = buildNormalizedPath(environment.get("TEMP"), "dub", "packages");
887         } else version(Posix){
888             systemDubPath = "/var/lib/dub/packages";
889             userDubPath = buildNormalizedPath(environment.get("HOME"), ".dub", "packages");
890             if(!userDubPath.isAbsolute)
891                 userDubPath = buildNormalizedPath(getcwd(), userDubPath);
892             tempPath = "/tmp/packages";
893         }
894     }
895 
896     /// find package file (dub.json, package.json) in specified dir; returns absoulute path to found file or null if not found
897     static string findPackageFile(string pathName) {
898         string fn = buildNormalizedPath(pathName, "dub.json");
899         if (fn.exists && fn.isFile)
900             return fn;
901         fn = buildNormalizedPath(pathName, "dub.sdl");
902         if (fn.exists && fn.isFile)
903             return fn;
904         fn = buildNormalizedPath(pathName, "package.json");
905         if (fn.exists && fn.isFile)
906             return fn;
907         return null;
908     }
909 
910     protected string findPackage(string packageDir, string packageName, string packageVersion) {
911         string fullName = packageVersion.startsWith("~") ? packageName ~ "-" ~ packageVersion[1..$] : packageName ~ "-" ~ packageVersion;
912         string pathName = absolutePath(buildNormalizedPath(packageDir, fullName));
913         if (pathName.exists && pathName.isDir) {
914             string fn = findPackageFile(pathName);
915             if (fn)
916                 return fn;
917             // new DUB support - with package subdirectory
918             fn = findPackageFile(buildNormalizedPath(pathName, packageName));
919             if (fn)
920                 return fn;
921         }
922         return null;
923     }
924 
925     string findPackage(string packageName, string packageVersion) {
926         string res = null;
927         res = findPackage(userDubPath, packageName, packageVersion);
928         if (res)
929             return res;
930         res = findPackage(systemDubPath, packageName, packageVersion);
931         return res;
932     }
933 }
934 
935 bool isValidProjectName(in string s) pure {
936     if (s.empty)
937         return false;
938     return reduce!((a, b) => a && (b == '_' || b == '-' || isAlphaNum(b)))(true, s);
939 }
940 
941 bool isValidModuleName(in string s) pure {
942     if (s.empty)
943         return false;
944     return reduce!((a, b) => a && (b == '_' || isAlphaNum(b)))(true, s);
945 }
946 
947 bool isValidFileName(in string s) pure {
948     if (s.empty)
949         return false;
950     return reduce!((a, b) => a && (b == '_' || b == '.' || b == '-' || isAlphaNum(b)))(true, s);
951 }
952 
953 unittest {
954     assert(!isValidProjectName(""));
955     assert(isValidProjectName("project"));
956     assert(isValidProjectName("cool_project"));
957     assert(isValidProjectName("project-2"));
958     assert(!isValidProjectName("project.png"));
959     assert(!isValidProjectName("[project]"));
960     assert(!isValidProjectName("<project/>"));
961     assert(!isValidModuleName(""));
962     assert(isValidModuleName("module"));
963     assert(isValidModuleName("awesome_module2"));
964     assert(!isValidModuleName("module-2"));
965     assert(!isValidModuleName("module.png"));
966     assert(!isValidModuleName("[module]"));
967     assert(!isValidModuleName("<module>"));
968     assert(!isValidFileName(""));
969     assert(isValidFileName("file"));
970     assert(isValidFileName("file_2"));
971     assert(isValidFileName("file-2"));
972     assert(isValidFileName("file.txt"));
973     assert(!isValidFileName("[file]"));
974     assert(!isValidFileName("<file>"));
975 }
976 
977 class EditorBookmark {
978     string file;
979     string fullFilePath;
980     string projectFilePath;
981     int line;
982     string projectName;
983 }
984 
985 
986 string[] splitByLines(string s) {
987     string[] res;
988     int start = 0;
989     for(int i = 0; i <= s.length; i++) {
990         if (i == s.length) {
991             if (start < i)
992                 res ~= s[start .. i];
993             break;
994         }
995         if (s[i] == '\r' || s[i] == '\n') {
996             if (start < i)
997                 res ~= s[start .. i];
998             start = i + 1;
999         }
1000     }
1001     return res;
1002 }
1003 
1004 struct CompilerImportPathsCache {
1005 
1006     private static class Entry {
1007         string[] list;
1008     }
1009     private Entry[string] _cache;
1010 
1011     string[] getImportPathsFor(string compiler) {
1012         import dlangui.core.files : findExecutablePath;
1013         if (!compiler.length)
1014             return [];
1015         if (auto p = compiler in _cache) {
1016             // found in cache
1017             return p.list;
1018         }
1019         Log.d("Searching for compiler path: ", compiler);
1020         import std.path : isAbsolute;
1021         string compilerPath = compiler;
1022         if (compiler == "default") {
1023             // try to autodetect default compiler
1024             compilerPath = findExecutablePath("dmd");
1025             if (!compilerPath)
1026                 compilerPath = findExecutablePath("ldc");
1027             if (!compilerPath)
1028                 compilerPath = findExecutablePath("gdc");
1029         } else if (compilerPath && !compilerPath.isAbsolute)
1030             compilerPath = findExecutablePath(compiler);
1031         string[] res;
1032         if (compilerPath)
1033             res = detectImportPathsForCompiler(compilerPath);
1034         else
1035             Log.w("Compiler executable not found for `", compiler, "`");
1036         Entry newItem = new Entry();
1037         newItem.list = res;
1038         _cache[compiler] = newItem;
1039         return res;
1040     }
1041 }
1042 
1043 __gshared CompilerImportPathsCache compilerImportPathsCache;
1044 
1045 string[] detectImportPathsForCompiler(string compiler) {
1046     string[] res;
1047     import std.process : pipeProcess, Redirect, wait;
1048     import std.string : startsWith, indexOf;
1049     import std.path : buildNormalizedPath;
1050     import std.file : write, remove;
1051     import dlangui.core.files;
1052     try {
1053         string sourcefilename = appDataPath(".dlangide") ~ PATH_DELIMITER ~ "tmp_dummy_file_to_get_import_paths.d";
1054         write(sourcefilename, "import module_that_does_not_exist;\n");
1055         auto pipes = pipeProcess([compiler, sourcefilename], Redirect.stdin | Redirect.stdout | Redirect.stderrToStdout, null, Config.suppressConsole);
1056         char[4096] buffer;
1057         char[] s = pipes.stdout.rawRead(buffer);
1058         wait(pipes.pid);
1059         //auto ls = execute([compiler, sourcefilename]);
1060         remove(sourcefilename);
1061         //string s = ls.output;
1062         string[] lines = splitByLines(cast(string)s);
1063         debug Log.d("compiler output:\n", s);
1064         foreach(line; lines) {
1065             if (line.startsWith("import path[")) {
1066                 auto p = line.indexOf("] = ");
1067                 if (p > 0) {
1068                     line = line[p + 4 .. $];
1069                     string path = line.buildNormalizedPath;
1070                     debug Log.d("import path found: `", path, "`");
1071                     res ~= path;
1072                 }
1073             }
1074         }
1075         return res;
1076     } catch (Exception e) {
1077         return null;
1078     }
1079 }