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 dstring name() { return _name; }
58 
59     @property string name8() {
60         return _name.toUTF8;
61     }
62 
63     /// returns true if item is folder
64     @property const bool isFolder() { return false; }
65     /// returns child object count
66     @property int childCount() { return 0; }
67     /// returns child item by index
68     ProjectItem child(int index) { return null; }
69 
70     void refresh() {
71     }
72 
73     ProjectSourceFile findSourceFile(string projectFileName, string fullFileName) {
74         if (fullFileName.equal(_filename))
75             return cast(ProjectSourceFile)this;
76         if (project && projectFileName.equal(project.absoluteToRelativePath(_filename)))
77             return cast(ProjectSourceFile)this;
78         return null;
79     }
80 
81     @property bool isDSourceFile() {
82         if (isFolder)
83             return false;
84         return filename.endsWith(".d") || filename.endsWith(".dd") || filename.endsWith(".dd")  || filename.endsWith(".di") || filename.endsWith(".dh") || filename.endsWith(".ddoc");
85     }
86 
87     @property bool isJsonFile() {
88         if (isFolder)
89             return false;
90         return filename.endsWith(".json") || filename.endsWith(".JSON");
91     }
92 
93     @property bool isDMLFile() {
94         if (isFolder)
95             return false;
96         return filename.endsWith(".dml") || filename.endsWith(".DML");
97     }
98 
99     @property bool isXMLFile() {
100         if (isFolder)
101             return false;
102         return filename.endsWith(".xml") || filename.endsWith(".XML");
103     }
104 }
105 
106 /// Project folder
107 class ProjectFolder : ProjectItem {
108     protected ObjectList!ProjectItem _children;
109 
110     this(string filename) {
111         super(filename);
112     }
113 
114     @property override const bool isFolder() {
115         return true;
116     }
117     @property override int childCount() {
118         return _children.count;
119     }
120     /// returns child item by index
121     override ProjectItem child(int index) {
122         return _children[index];
123     }
124     void addChild(ProjectItem item) {
125         _children.add(item);
126         item._parent = this;
127         item._project = _project;
128     }
129     ProjectItem childByPathName(string path) {
130         for (int i = 0; i < _children.count; i++) {
131             if (_children[i].filename.equal(path))
132                 return _children[i];
133         }
134         return null;
135     }
136     ProjectItem childByName(dstring s) {
137         for (int i = 0; i < _children.count; i++) {
138             if (_children[i].name.equal(s))
139                 return _children[i];
140         }
141         return null;
142     }
143 
144     override ProjectSourceFile findSourceFile(string projectFileName, string fullFileName) {
145         for (int i = 0; i < _children.count; i++) {
146             if (ProjectSourceFile res = _children[i].findSourceFile(projectFileName, fullFileName))
147                 return res;
148         }
149         return null;
150     }
151 
152     bool loadDir(string path) {
153         string src = relativeToAbsolutePath(path);
154         if (exists(src) && isDir(src)) {
155             ProjectFolder existing = cast(ProjectFolder)childByPathName(src);
156             if (existing) {
157                 if (existing.isFolder)
158                     existing.loadItems();
159                 return true;
160             }
161             auto dir = new ProjectFolder(src);
162             addChild(dir);
163             Log.d("    added project folder ", src);
164             dir.loadItems();
165             return true;
166         }
167         return false;
168     }
169 
170     bool loadFile(string path) {
171         string src = relativeToAbsolutePath(path);
172         if (exists(src) && isFile(src)) {
173             ProjectItem existing = childByPathName(src);
174             if (existing)
175                 return true;
176             auto f = new ProjectSourceFile(src);
177             addChild(f);
178             Log.d("    added project file ", src);
179             return true;
180         }
181         return false;
182     }
183 
184     void loadItems() {
185         bool[string] loaded;
186         string path = _filename;
187         if (exists(path) && isFile(path))
188             path = dirName(path);
189         foreach(e; dirEntries(path, SpanMode.shallow)) {
190             string fn = baseName(e.name);
191             if (e.isDir) {
192                 loadDir(fn);
193                 loaded[fn] = true;
194             } else if (e.isFile) {
195                 loadFile(fn);
196                 loaded[fn] = true;
197             }
198         }
199         // removing non-reloaded items
200         for (int i = _children.count - 1; i >= 0; i--) {
201             if (!(toUTF8(_children[i].name) in loaded)) {
202                 _children.remove(i);
203             }
204         }
205     }
206 
207     string relativeToAbsolutePath(string path) {
208         if (isAbsolute(path))
209             return path;
210         string fn = _filename;
211         if (exists(fn) && isFile(fn))
212             fn = dirName(fn);
213         return buildNormalizedPath(fn, path);
214     }
215 
216     override void refresh() {
217         loadItems();
218     }
219 }
220 
221 /// Project source file
222 class ProjectSourceFile : ProjectItem {
223     this(string filename) {
224         super(filename);
225     }
226     /// file path relative to project directory
227     @property string projectFilePath() {
228         return project.absoluteToRelativePath(filename);
229     }
230 }
231 
232 class WorkspaceItem {
233     protected string _filename;
234     protected string _dir;
235     protected dstring _name;
236     protected dstring _originalName;
237     protected dstring _description;
238 
239     this(string fname = null) {
240         filename = fname;
241     }
242 
243     /// file name of workspace item
244     @property string filename() { return _filename; }
245 
246     /// workspace item directory
247     @property string dir() { return _dir; }
248 
249     /// file name of workspace item
250     @property void filename(string fname) {
251         if (fname.length > 0) {
252             _filename = buildNormalizedPath(fname);
253             _dir = dirName(filename);
254         } else {
255             _filename = null;
256             _dir = null;
257         }
258     }
259 
260     /// name
261     @property dstring name() { return _name; }
262 
263     @property string name8() {
264         return _name.toUTF8;
265     }
266 
267     /// name
268     @property void name(dstring s) {  _name = s; }
269 
270     /// description
271     @property dstring description() { return _description; }
272     /// description
273     @property void description(dstring s) { _description = s; }
274 
275     /// load
276     bool load(string fname) {
277         // override it
278         return false;
279     }
280 
281     bool save(string fname = null) {
282         return false;
283     }
284 }
285 
286 /// detect DMD source paths
287 string[] dmdSourcePaths() {
288     string[] res;
289 
290 	if(!includePath.empty){
291 		res ~= includePath;
292 	}
293 
294     version(Windows) {
295         import dlangui.core.files;
296         string dmdPath = findExecutablePath("dmd");
297         if (dmdPath) {
298             string dmdDir = buildNormalizedPath(dirName(dmdPath), "..", "..", "src");
299             res ~= absolutePath(buildNormalizedPath(dmdDir, "druntime", "import"));
300             res ~= absolutePath(buildNormalizedPath(dmdDir, "phobos"));
301         }
302     } else {
303         res ~= "/usr/include/dmd/druntime/import";
304         res ~= "/usr/include/dmd/phobos";
305     }
306     return res;
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[string] load(Setting s)
342     {
343         ProjectConfiguration[string] res = [DEFAULT_NAME: DEFAULT];
344         Setting configs = s.objectByPath("configurations");
345         if(configs is null || configs.type != SettingType.ARRAY) 
346             return res;
347         
348         foreach(conf; configs) {
349             if(!conf.isObject) continue;
350             Type t = Type.Default;
351             if(auto typeName = conf.getString("targetType"))
352                 t = parseType(typeName);
353             if (string confName = conf.getString("name"))
354                 res[confName] = ProjectConfiguration(confName, t);
355         }
356         return res;
357     }
358 }
359 
360 /// DLANGIDE D project
361 class Project : WorkspaceItem {
362     protected Workspace _workspace;
363     protected bool _opened;
364     protected ProjectFolder _items;
365     protected ProjectSourceFile _mainSourceFile;
366     protected SettingsFile _projectFile;
367     protected ProjectSettings _settingsFile;
368     protected bool _isDependency;
369     protected bool _isSubproject;
370     protected bool _isEmbeddedSubproject;
371     protected dstring _baseProjectName;
372     protected string _dependencyVersion;
373 
374     protected string[] _sourcePaths;
375     protected string[] _builderSourcePaths;
376     protected ProjectConfiguration[string] _configurations;
377 
378     this(Workspace ws, string fname = null, string dependencyVersion = null) {
379         super(fname);
380         _workspace = ws;
381 
382         if (_workspace) {
383     		foreach(obj; _workspace.includePath.array)
384     			includePath ~= obj.str;
385         }
386 
387         _items = new ProjectFolder(fname);
388         _dependencyVersion = dependencyVersion;
389         _isDependency = _dependencyVersion.length > 0;
390         _projectFile = new SettingsFile(fname);
391     }
392 
393     void setBaseProject(Project p) {
394         if (p) {
395             _isSubproject = true;
396             _isDependency = p._isDependency;
397             _baseProjectName = p._originalName;
398             _dependencyVersion = p._dependencyVersion;
399         } else {
400             _isSubproject = false;
401         }
402     }
403 
404     void setSubprojectJson(Setting s) {
405         if (!_projectFile)
406             _projectFile = new SettingsFile();
407         _isEmbeddedSubproject = true;
408         _projectFile.replaceSetting(s.clone);
409     }
410 
411     @property ProjectSettings settings() {
412         if (!_settingsFile) {
413             _settingsFile = new ProjectSettings(settingsFileName);
414             _settingsFile.updateDefaults();
415             _settingsFile.load();
416             _settingsFile.save();
417         }
418         return _settingsFile;
419     }
420 
421     @property string settingsFileName() {
422         return buildNormalizedPath(dir, toUTF8(name) ~ ".settings");
423     }
424 
425     @property bool isDependency() { return _isDependency; }
426     @property string dependencyVersion() { return _dependencyVersion; }
427 
428     /// returns project configurations
429     @property const(ProjectConfiguration[string]) configurations() const
430     {
431         return _configurations;
432     }
433 
434     /// direct access to project file (json)
435     @property SettingsFile content() { return _projectFile; }
436 
437     /// name
438     override @property dstring name() {
439         return super.name();
440     }
441 
442     /// name
443     override @property void name(dstring s) {
444         super.name(s);
445         _projectFile.setString("name", toUTF8(s));
446     }
447 
448     /// name
449     override @property dstring description() {
450         return super.description();
451     }
452 
453     /// name
454     override @property void description(dstring s) {
455         super.description(s);
456         _projectFile.setString("description", toUTF8(s));
457     }
458 
459     /// returns project's own source paths
460     @property string[] sourcePaths() { return _sourcePaths; }
461     /// returns project's own source paths
462     @property string[] builderSourcePaths() { 
463         if (!_builderSourcePaths) {
464             _builderSourcePaths = dmdSourcePaths();
465         }
466         return _builderSourcePaths; 
467     }
468 
469     ProjectSourceFile findSourceFile(string projectFileName, string fullFileName) {
470         return _items ? _items.findSourceFile(projectFileName, fullFileName) : null;
471     }
472 
473     private static void addUnique(ref string[] dst, string[] items) {
474         foreach(item; items) {
475             if (!canFind(dst, item))
476                 dst ~= item;
477         }
478     }
479     @property string[] importPaths() {
480         string[] res;
481         addUnique(res, sourcePaths);
482         addUnique(res, builderSourcePaths);
483         foreach(dep; _dependencies) {
484             addUnique(res, dep.sourcePaths);
485         }
486         return res;
487     }
488 
489     string relativeToAbsolutePath(string path) {
490         if (isAbsolute(path))
491             return path;
492         return buildNormalizedPath(_dir, path);
493     }
494 
495     string absoluteToRelativePath(string path) {
496         if (!isAbsolute(path))
497             return path;
498         return relativePath(path, _dir);
499     }
500 
501     @property ProjectSourceFile mainSourceFile() { return _mainSourceFile; }
502     @property ProjectFolder items() { return _items; }
503 
504     @property Workspace workspace() { return _workspace; }
505 
506     @property void workspace(Workspace p) { _workspace = p; }
507 
508     @property string defWorkspaceFile() {
509         return buildNormalizedPath(_filename.dirName, toUTF8(name) ~ WORKSPACE_EXTENSION);
510     }
511 
512     @property bool isExecutable() {
513         // TODO: use targetType
514         return true;
515     }
516 
517     /// return executable file name, or null if it's library project or executable is not found
518     @property string executableFileName() {
519         if (!isExecutable)
520             return null;
521         string exename = toUTF8(name);
522         exename = _projectFile.getString("targetName", exename);
523         // TODO: use targetName
524         version (Windows) {
525             exename = exename ~ ".exe";
526         }
527         string targetPath = _projectFile.getString("targetPath", null);
528         string exePath;
529         if (targetPath.length)
530             exePath = buildNormalizedPath(_filename.dirName, targetPath, exename); // int $targetPath directory
531         else
532             exePath = buildNormalizedPath(_filename.dirName, exename); // in project directory
533         return exePath;
534     }
535 
536     /// working directory for running and debugging project
537     @property string workingDirectory() {
538         // TODO: get from settings
539         return _filename.dirName;
540     }
541 
542     /// commandline parameters for running and debugging project
543     @property string runArgs() {
544         // TODO: get from settings
545         return null;
546     }
547 
548     @property bool runInExternalConsole() {
549         // TODO
550         return settings.runInExternalConsole;
551     }
552 
553     ProjectFolder findItems(string[] srcPaths) {
554         auto folder = new ProjectFolder(_filename);
555         folder.project = this;
556         foreach(customPath; srcPaths) {
557             string path = relativeToAbsolutePath(customPath);
558             if (folder.loadDir(path))
559                 _sourcePaths ~= path;
560         }
561         return folder;
562     }
563 
564     void refresh() {
565         for (int i = _items._children.count - 1; i >= 0; i--) {
566             if (_items._children[i].isFolder)
567                 _items._children[i].refresh();
568         }
569     }
570 
571     void findMainSourceFile() {
572         string n = toUTF8(name);
573         string[] mainnames = ["app.d", "main.d", n ~ ".d"];
574         foreach(sname; mainnames) {
575             _mainSourceFile = findSourceFileItem(buildNormalizedPath(_dir, "src", sname));
576             if (_mainSourceFile)
577                 break;
578             _mainSourceFile = findSourceFileItem(buildNormalizedPath(_dir, "source", sname));
579             if (_mainSourceFile)
580                 break;
581         }
582     }
583 
584     /// tries to find source file in project, returns found project source file item, or null if not found
585     ProjectSourceFile findSourceFileItem(ProjectItem dir, string filename, bool fullFileName=true) {
586         foreach(i; 0 .. dir.childCount) {
587             ProjectItem item = dir.child(i);
588             if (item.isFolder) {
589                 ProjectSourceFile res = findSourceFileItem(item, filename, fullFileName);
590                 if (res)
591                     return res;
592             } else {
593                 auto res = cast(ProjectSourceFile)item;
594                 if(res)
595                 {
596                     if(fullFileName && res.filename.equal(filename))
597                         return res;
598                     else if (!fullFileName && res.filename.endsWith(filename))
599                         return res;
600                 }
601             }
602         }
603         return null;
604     }
605 
606     ProjectSourceFile findSourceFileItem(string filename, bool fullFileName=true) {
607         return findSourceFileItem(_items, filename, fullFileName);
608     }
609 
610     protected Project[] _subPackages;
611 
612     /// add item to string array ignoring duplicates
613     protected static void addUnique(ref string[] list, string item) {
614         foreach(s; list)
615             if (s == item)
616                 return;
617         list ~= item;
618     }
619 
620     /// add item to string array ignoring duplicates
621     protected void addRelativePathIfExists(ref string[] list, string item) {
622         item = relativeToAbsolutePath(item);
623         if (item.exists && item.isDir)
624             addUnique(list, item);
625     }
626 
627     /// find source paths for project
628     protected string[] findSourcePaths() {
629         string[] res;
630         res.assumeSafeAppend;
631         string[] srcPaths = _projectFile.getStringArray("sourcePaths");
632         foreach(s; srcPaths)
633             addRelativePathIfExists(res, s);
634         Setting configs = _projectFile.objectByPath("configurations");
635         if (configs) {
636             for (int i = 0; i < configs.length; i++) {
637                 Setting s = configs[i];
638                 if (s) {
639                     string[] paths = s.getStringArray("sourcePaths");
640                     foreach(path; paths)
641                         addRelativePathIfExists(res, path);
642                 }
643             }
644         }
645         if (!res.length) {
646             addRelativePathIfExists(res, "src");
647             addRelativePathIfExists(res, "source");
648         }
649 
650         return res;
651     }
652 
653     void processSubpackages() {
654         import dlangui.core.files;
655         _subPackages.length = 0;
656         Setting subPackages = _projectFile.settingByPath("subPackages", SettingType.ARRAY, false);
657         if (subPackages) {
658             string p = _projectFile.filename.dirName;
659             for(int i = 0; i < subPackages.length; i++) {
660                 Setting sp = subPackages[i];
661                 if (sp.isString) {
662                     // string
663                     string path = convertPathDelimiters(sp.str);
664                     string relative = relativePath(path, p);
665                     path = buildNormalizedPath(absolutePath(relative, p));
666                     //Log.d("Subproject path: ", path);
667                     string fn = DubPackageFinder.findPackageFile(path);
668                     //Log.d("Subproject file: ", fn);
669                     Project prj = new Project(_workspace, fn);
670                     prj.setBaseProject(this);
671                     if (prj.load()) {
672                         Log.d("Loaded subpackage from file: ", fn);
673                         _subPackages ~= prj;
674                         if (_workspace)
675                             _workspace.addDependencyProject(prj);
676                     } else {
677                         Log.w("Failed to load subpackage from file: ", fn);
678                     }
679                 } else if (sp.isObject) {
680                     // object - file inside base project dub.json
681                     Log.d("Subpackage is JSON object");
682                     string subname = sp.getString("name");
683                     if (subname) {
684                         Project prj = new Project(_workspace, _filename ~ "@" ~ subname);
685                         prj.setBaseProject(this);
686                         prj.setSubprojectJson(sp);
687                         bool res = prj.processLoadedProject();
688                         if (res) {
689                             Log.d("Added embedded subpackage ", subname);
690                             _subPackages ~= prj;
691                             if (_workspace)
692                                 _workspace.addDependencyProject(prj);
693                         } else {
694                             Log.w("Error while processing embedded subpackage");
695                         }
696                     }
697                 }
698             }
699         }
700     }
701 
702     /// parse data from _projectFile after loading
703     bool processLoadedProject() {
704         //
705         _mainSourceFile = null;
706         try {
707             _name = toUTF32(_projectFile.getString("name"));
708             _originalName = _name;
709             if (_baseProjectName) {
710                 _name = _baseProjectName ~ ":" ~ _name;
711             }
712             if (_isDependency) {
713                 _name ~= "-"d;
714                 _name ~= toUTF32(_dependencyVersion.startsWith("~") ? _dependencyVersion[1..$] : _dependencyVersion);
715             }
716             _description = toUTF32(_projectFile.getString("description"));
717             Log.d("  project name: ", _name);
718             Log.d("  project description: ", _description);
719 
720             processSubpackages();
721 
722             string[] srcPaths = findSourcePaths();
723             _sourcePaths = null;
724             _items = findItems(srcPaths);
725             findMainSourceFile();
726 
727             Log.i("Project source paths: ", sourcePaths);
728             Log.i("Builder source paths: ", builderSourcePaths);
729             if (!_isDependency)
730                 loadSelections();
731 
732             _configurations = ProjectConfiguration.load(_projectFile);
733             Log.i("Project configurations: ", _configurations);
734 
735 
736         } catch (Exception e) {
737             Log.e("Cannot read project file", e);
738             return false;
739         }
740         _items.loadFile(filename);
741         return true;
742     }
743 
744     override bool load(string fname = null) {
745         if (!_projectFile)
746             _projectFile = new SettingsFile();
747         if (fname.length > 0)
748             filename = fname;
749         if (!_projectFile.load(_filename)) {
750             Log.e("failed to load project from file ", _filename);
751             return false;
752         }
753         Log.d("Reading project from file ", _filename);
754         return processLoadedProject();
755 
756     }
757 
758     override bool save(string fname = null) {
759         if (_isEmbeddedSubproject)
760             return false;
761         if (fname !is null)
762             filename = fname;
763         assert(filename !is null);
764         return _projectFile.save(filename, true);
765     }
766 
767     protected Project[] _dependencies;
768     @property Project[] dependencies() { return _dependencies; }
769 
770     Project findDependencyProject(string filename) {
771         foreach(dep; _dependencies) {
772             if (dep.filename.equal(filename))
773                 return dep;
774         }
775         return null;
776     }
777 
778     bool loadSelections() {
779         Project[] newdeps;
780         _dependencies.length = 0;
781         auto finder = new DubPackageFinder;
782         scope(exit) destroy(finder);
783         SettingsFile selectionsFile = new SettingsFile(buildNormalizedPath(_dir, "dub.selections.json"));
784         if (!selectionsFile.load()) {
785             _dependencies = newdeps;
786             return false;
787         }
788         Setting versions = selectionsFile.objectByPath("versions");
789         if (!versions.isObject) {
790             _dependencies = newdeps;
791             return false;
792         }
793         string[string] versionMap = versions.strMap;
794         foreach(packageName, packageVersion; versionMap) {
795             string fn = finder.findPackage(packageName, packageVersion);
796             Log.d("dependency ", packageName, " ", packageVersion, " : ", fn ? fn : "NOT FOUND");
797             if (fn) {
798                 Project p = findDependencyProject(fn);
799                 if (p) {
800                     Log.d("Found existing dependency project ", fn);
801                     newdeps ~= p;
802                     continue;
803                 }
804                 p = new Project(_workspace, fn, packageVersion);
805                 if (p.load()) {
806                     newdeps ~= p;
807                     if (_workspace)
808                         _workspace.addDependencyProject(p);
809                 } else {
810                     Log.e("cannot load dependency package ", packageName, " ", packageVersion, " from file ", fn);
811                     destroy(p);
812                 }
813             }
814         }
815         _dependencies = newdeps;
816         return true;
817     }
818 }
819 
820 class DubPackageFinder {
821     string systemDubPath;
822     string userDubPath;
823     string tempPath;
824     this() {
825         version(Windows){
826             systemDubPath = buildNormalizedPath(environment.get("ProgramData"), "dub", "packages");
827             userDubPath = buildNormalizedPath(environment.get("APPDATA"), "dub", "packages");
828             tempPath = buildNormalizedPath(environment.get("TEMP"), "dub", "packages");
829         } else version(Posix){
830             systemDubPath = "/var/lib/dub/packages";
831             userDubPath = buildNormalizedPath(environment.get("HOME"), ".dub", "packages");
832             if(!userDubPath.isAbsolute)
833                 userDubPath = buildNormalizedPath(getcwd(), userDubPath);
834             tempPath = "/tmp/packages";
835         }
836     }
837 
838     /// find package file (dub.json, package.json) in specified dir; returns absoulute path to found file or null if not found
839     static string findPackageFile(string pathName) {
840         string fn = buildNormalizedPath(pathName, "dub.json");
841         if (fn.exists && fn.isFile)
842             return fn;
843         fn = buildNormalizedPath(pathName, "dub.sdl");
844         if (fn.exists && fn.isFile)
845             return fn;
846         fn = buildNormalizedPath(pathName, "package.json");
847         if (fn.exists && fn.isFile)
848             return fn;
849         return null;
850     }
851 
852     protected string findPackage(string packageDir, string packageName, string packageVersion) {
853         string fullName = packageVersion.startsWith("~") ? packageName ~ "-" ~ packageVersion[1..$] : packageName ~ "-" ~ packageVersion;
854         string pathName = absolutePath(buildNormalizedPath(packageDir, fullName));
855         if (pathName.exists && pathName.isDir) {
856             string fn = findPackageFile(pathName);
857             if (fn)
858                 return fn;
859             // new DUB support - with package subdirectory
860             fn = findPackageFile(buildNormalizedPath(pathName, packageName));
861             if (fn)
862                 return fn;
863         }
864         return null;
865     }
866 
867     string findPackage(string packageName, string packageVersion) {
868         string res = null;
869         res = findPackage(userDubPath, packageName, packageVersion);
870         if (res)
871             return res;
872         res = findPackage(systemDubPath, packageName, packageVersion);
873         return res;
874     }
875 }
876 
877 bool isValidProjectName(in string s) pure {
878     if (s.empty)
879         return false;
880     return reduce!q{ a && (b == '_' || b == '-' || std.ascii.isAlphaNum(b)) }(true, s);
881 }
882 
883 bool isValidModuleName(in string s) pure {
884     if (s.empty)
885         return false;
886     return reduce!q{ a && (b == '_' || std.ascii.isAlphaNum(b)) }(true, s);
887 }
888 
889 bool isValidFileName(in string s) pure {
890     if (s.empty)
891         return false;
892     return reduce!q{ a && (b == '_' || b == '.' || b == '-' || std.ascii.isAlphaNum(b)) }(true, s);
893 }
894 
895 unittest {
896     assert(!isValidProjectName(""));
897     assert(isValidProjectName("project"));
898     assert(isValidProjectName("cool_project"));
899     assert(isValidProjectName("project-2"));
900     assert(!isValidProjectName("project.png"));
901     assert(!isValidProjectName("[project]"));
902     assert(!isValidProjectName("<project/>"));
903     assert(!isValidModuleName(""));
904     assert(isValidModuleName("module"));
905     assert(isValidModuleName("awesome_module2"));
906     assert(!isValidModuleName("module-2"));
907     assert(!isValidModuleName("module.png"));
908     assert(!isValidModuleName("[module]"));
909     assert(!isValidModuleName("<module>"));
910     assert(!isValidFileName(""));
911     assert(isValidFileName("file"));
912     assert(isValidFileName("file_2"));
913     assert(isValidFileName("file-2"));
914     assert(isValidFileName("file.txt"));
915     assert(!isValidFileName("[file]"));
916     assert(!isValidFileName("<file>"));
917 }
918 
919 class EditorBookmark {
920     string file;
921     string fullFilePath;
922     string projectFilePath;
923     int line;
924     string projectName;
925 }