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