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         sortItems();
206     }
207 
208     /// predicate for sorting project items
209     static bool compareProjectItemsLess(ProjectItem item1, ProjectItem item2) {
210         return ((item1.isFolder && !item2.isFolder) || ((item1.isFolder == item2.isFolder) && (item1.name < item2.name)));
211     }
212 
213     void sortItems() {
214         import std.algorithm.sorting : sort;
215         sort!compareProjectItemsLess(_children.asArray);
216     }
217 
218     string relativeToAbsolutePath(string path) {
219         if (isAbsolute(path))
220             return path;
221         string fn = _filename;
222         if (exists(fn) && isFile(fn))
223             fn = dirName(fn);
224         return buildNormalizedPath(fn, path);
225     }
226 
227     override void refresh() {
228         loadItems();
229     }
230 }
231 
232 
233 /// Project source file
234 class ProjectSourceFile : ProjectItem {
235     this(string filename) {
236         super(filename);
237     }
238     /// file path relative to project directory
239     @property string projectFilePath() {
240         return project.absoluteToRelativePath(filename);
241     }
242 
243     void setFilename(string filename) {
244         _filename = buildNormalizedPath(filename);
245         _name = toUTF32(baseName(_filename));
246     }
247 }
248 
249 class WorkspaceItem {
250     protected string _filename;
251     protected string _dir;
252     protected dstring _name;
253     protected dstring _originalName;
254     protected dstring _description;
255 
256     this(string fname = null) {
257         filename = fname;
258     }
259 
260     /// file name of workspace item
261     @property string filename() { return _filename; }
262 
263     /// workspace item directory
264     @property string dir() { return _dir; }
265 
266     /// file name of workspace item
267     @property void filename(string fname) {
268         if (fname.length > 0) {
269             _filename = buildNormalizedPath(fname);
270             _dir = dirName(filename);
271         } else {
272             _filename = null;
273             _dir = null;
274         }
275     }
276 
277     /// name
278     @property dstring name() { return _name; }
279 
280     @property string name8() {
281         return _name.toUTF8;
282     }
283 
284     /// name
285     @property void name(dstring s) {  _name = s; }
286 
287     /// description
288     @property dstring description() { return _description; }
289     /// description
290     @property void description(dstring s) { _description = s; }
291 
292     /// load
293     bool load(string fname) {
294         // override it
295         return false;
296     }
297 
298     bool save(string fname = null) {
299         return false;
300     }
301 }
302 
303 /// detect DMD source paths
304 string[] dmdSourcePaths() {
305     string[] res;
306 
307 	if(!includePath.empty){
308 		res ~= includePath;
309 	}
310 
311     version(Windows) {
312         import dlangui.core.files;
313         string dmdPath = findExecutablePath("dmd");
314         if (dmdPath) {
315             string dmdDir = buildNormalizedPath(dirName(dmdPath), "..", "..", "src");
316             res ~= absolutePath(buildNormalizedPath(dmdDir, "druntime", "import"));
317             res ~= absolutePath(buildNormalizedPath(dmdDir, "phobos"));
318         }
319     } else {
320         res ~= "/usr/include/dmd/druntime/import";
321         res ~= "/usr/include/dmd/phobos";
322     }
323     return res;
324 }
325 
326 /// Stores info about project configuration
327 struct ProjectConfiguration {
328     /// name used to build the project
329     string name;
330     /// type, for libraries one can run tests, for apps - execute them
331     Type type;
332     
333     /// How to display default configuration in ui
334     immutable static string DEFAULT_NAME = "default";
335     /// Default project configuration
336     immutable static ProjectConfiguration DEFAULT = ProjectConfiguration(DEFAULT_NAME, Type.Default);
337     
338     /// Type of configuration
339     enum Type {
340         Default,
341         Executable,
342         Library
343     }
344     
345     private static Type parseType(string s)
346     {
347         switch(s)
348         {
349             case "executable": return Type.Executable;
350             case "library": return Type.Library;
351             case "dynamicLibrary": return Type.Library;
352             case "staticLibrary": return Type.Library;
353             default: return Type.Default;
354         }
355     }
356     
357     /// parsing from setting file
358     static ProjectConfiguration[string] load(Setting s)
359     {
360         ProjectConfiguration[string] res = [DEFAULT_NAME: DEFAULT];
361         Setting configs = s.objectByPath("configurations");
362         if(configs is null || configs.type != SettingType.ARRAY) 
363             return res;
364         
365         foreach(conf; configs) {
366             if(!conf.isObject) continue;
367             Type t = Type.Default;
368             if(auto typeName = conf.getString("targetType"))
369                 t = parseType(typeName);
370             if (string confName = conf.getString("name"))
371                 res[confName] = ProjectConfiguration(confName, t);
372         }
373         return res;
374     }
375 }
376 
377 /// DLANGIDE D project
378 class Project : WorkspaceItem {
379     protected Workspace _workspace;
380     protected bool _opened;
381     protected ProjectFolder _items;
382     protected ProjectSourceFile _mainSourceFile;
383     protected SettingsFile _projectFile;
384     protected ProjectSettings _settingsFile;
385     protected bool _isDependency;
386     protected bool _isSubproject;
387     protected bool _isEmbeddedSubproject;
388     protected dstring _baseProjectName;
389     protected string _dependencyVersion;
390 
391     protected string[] _sourcePaths;
392     protected string[] _builderSourcePaths;
393     protected ProjectConfiguration[string] _configurations;
394 
395     this(Workspace ws, string fname = null, string dependencyVersion = null) {
396         super(fname);
397         _workspace = ws;
398 
399         if (_workspace) {
400     		foreach(obj; _workspace.includePath.array)
401     			includePath ~= obj.str;
402         }
403 
404         _items = new ProjectFolder(fname);
405         _dependencyVersion = dependencyVersion;
406         _isDependency = _dependencyVersion.length > 0;
407         _projectFile = new SettingsFile(fname);
408     }
409 
410     void setBaseProject(Project p) {
411         if (p) {
412             _isSubproject = true;
413             _isDependency = p._isDependency;
414             _baseProjectName = p._originalName;
415             _dependencyVersion = p._dependencyVersion;
416         } else {
417             _isSubproject = false;
418         }
419     }
420 
421     void setSubprojectJson(Setting s) {
422         if (!_projectFile)
423             _projectFile = new SettingsFile();
424         _isEmbeddedSubproject = true;
425         _projectFile.replaceSetting(s.clone);
426     }
427 
428     @property ProjectSettings settings() {
429         if (!_settingsFile) {
430             _settingsFile = new ProjectSettings(settingsFileName);
431             _settingsFile.updateDefaults();
432             _settingsFile.load();
433             _settingsFile.save();
434         }
435         return _settingsFile;
436     }
437 
438     @property string settingsFileName() {
439         return buildNormalizedPath(dir, toUTF8(name) ~ ".settings");
440     }
441 
442     @property bool isDependency() { return _isDependency; }
443     @property string dependencyVersion() { return _dependencyVersion; }
444 
445     /// returns project configurations
446     @property const(ProjectConfiguration[string]) configurations() const
447     {
448         return _configurations;
449     }
450 
451     /// direct access to project file (json)
452     @property SettingsFile content() { return _projectFile; }
453 
454     /// name
455     override @property dstring name() {
456         return super.name();
457     }
458 
459     /// name
460     override @property void name(dstring s) {
461         super.name(s);
462         _projectFile.setString("name", toUTF8(s));
463     }
464 
465     /// name
466     override @property dstring description() {
467         return super.description();
468     }
469 
470     /// name
471     override @property void description(dstring s) {
472         super.description(s);
473         _projectFile.setString("description", toUTF8(s));
474     }
475 
476     /// returns project's own source paths
477     @property string[] sourcePaths() { return _sourcePaths; }
478     /// returns project's own source paths
479     @property string[] builderSourcePaths() { 
480         if (!_builderSourcePaths) {
481             _builderSourcePaths = dmdSourcePaths();
482         }
483         return _builderSourcePaths; 
484     }
485 
486     /// returns first source folder for project or null if not found
487     ProjectFolder firstSourceFolder() {
488         for(int i = 0; i < _items.childCount; i++) {
489             if (_items.child(i).isFolder)
490                 return cast(ProjectFolder)_items.child(i);
491         }
492         return null;
493     }
494 
495     ProjectSourceFile findSourceFile(string projectFileName, string fullFileName) {
496         return _items ? _items.findSourceFile(projectFileName, fullFileName) : null;
497     }
498 
499     private static void addUnique(ref string[] dst, string[] items) {
500         foreach(item; items) {
501             if (!canFind(dst, item))
502                 dst ~= item;
503         }
504     }
505     @property string[] importPaths() {
506         string[] res;
507         addUnique(res, sourcePaths);
508         addUnique(res, builderSourcePaths);
509         foreach(dep; _dependencies) {
510             addUnique(res, dep.sourcePaths);
511         }
512         return res;
513     }
514 
515     string relativeToAbsolutePath(string path) {
516         if (isAbsolute(path))
517             return path;
518         return buildNormalizedPath(_dir, path);
519     }
520 
521     string absoluteToRelativePath(string path) {
522         if (!isAbsolute(path))
523             return path;
524         return relativePath(path, _dir);
525     }
526 
527     @property ProjectSourceFile mainSourceFile() { return _mainSourceFile; }
528     @property ProjectFolder items() { return _items; }
529 
530     @property Workspace workspace() { return _workspace; }
531 
532     @property void workspace(Workspace p) { _workspace = p; }
533 
534     @property string defWorkspaceFile() {
535         return buildNormalizedPath(_filename.dirName, toUTF8(name) ~ WORKSPACE_EXTENSION);
536     }
537 
538     @property bool isExecutable() {
539         // TODO: use targetType
540         return true;
541     }
542 
543     /// return executable file name, or null if it's library project or executable is not found
544     @property string executableFileName() {
545         if (!isExecutable)
546             return null;
547         string exename = toUTF8(name);
548         exename = _projectFile.getString("targetName", exename);
549         // TODO: use targetName
550         version (Windows) {
551             exename = exename ~ ".exe";
552         }
553         string targetPath = _projectFile.getString("targetPath", null);
554         string exePath;
555         if (targetPath.length)
556             exePath = buildNormalizedPath(_filename.dirName, targetPath, exename); // int $targetPath directory
557         else
558             exePath = buildNormalizedPath(_filename.dirName, exename); // in project directory
559         return exePath;
560     }
561 
562     /// working directory for running and debugging project
563     @property string workingDirectory() {
564         // TODO: get from settings
565         return _filename.dirName;
566     }
567 
568     /// commandline parameters for running and debugging project
569     @property string runArgs() {
570         // TODO: get from settings
571         return null;
572     }
573 
574     @property bool runInExternalConsole() {
575         // TODO
576         return settings.runInExternalConsole;
577     }
578 
579     ProjectFolder findItems(string[] srcPaths) {
580         auto folder = new ProjectFolder(_filename);
581         folder.project = this;
582         foreach(customPath; srcPaths) {
583             string path = relativeToAbsolutePath(customPath);
584             if (folder.loadDir(path))
585                 _sourcePaths ~= path;
586         }
587         return folder;
588     }
589 
590     void refresh() {
591         for (int i = _items._children.count - 1; i >= 0; i--) {
592             if (_items._children[i].isFolder)
593                 _items._children[i].refresh();
594         }
595     }
596 
597     void findMainSourceFile() {
598         string n = toUTF8(name);
599         string[] mainnames = ["app.d", "main.d", n ~ ".d"];
600         foreach(sname; mainnames) {
601             _mainSourceFile = findSourceFileItem(buildNormalizedPath(_dir, "src", sname));
602             if (_mainSourceFile)
603                 break;
604             _mainSourceFile = findSourceFileItem(buildNormalizedPath(_dir, "source", sname));
605             if (_mainSourceFile)
606                 break;
607         }
608     }
609 
610     /// tries to find source file in project, returns found project source file item, or null if not found
611     ProjectSourceFile findSourceFileItem(ProjectItem dir, string filename, bool fullFileName=true) {
612         foreach(i; 0 .. dir.childCount) {
613             ProjectItem item = dir.child(i);
614             if (item.isFolder) {
615                 ProjectSourceFile res = findSourceFileItem(item, filename, fullFileName);
616                 if (res)
617                     return res;
618             } else {
619                 auto res = cast(ProjectSourceFile)item;
620                 if(res)
621                 {
622                     if(fullFileName && res.filename.equal(filename))
623                         return res;
624                     else if (!fullFileName && res.filename.endsWith(filename))
625                         return res;
626                 }
627             }
628         }
629         return null;
630     }
631 
632     ProjectSourceFile findSourceFileItem(string filename, bool fullFileName=true) {
633         return findSourceFileItem(_items, filename, fullFileName);
634     }
635 
636     protected Project[] _subPackages;
637 
638     /// add item to string array ignoring duplicates
639     protected static void addUnique(ref string[] list, string item) {
640         foreach(s; list)
641             if (s == item)
642                 return;
643         list ~= item;
644     }
645 
646     /// add item to string array ignoring duplicates
647     protected void addRelativePathIfExists(ref string[] list, string item) {
648         item = relativeToAbsolutePath(item);
649         if (item.exists && item.isDir)
650             addUnique(list, item);
651     }
652 
653     /// find source paths for project
654     protected string[] findSourcePaths() {
655         string[] res;
656         res.assumeSafeAppend;
657         string[] srcPaths = _projectFile.getStringArray("sourcePaths");
658         foreach(s; srcPaths)
659             addRelativePathIfExists(res, s);
660         Setting configs = _projectFile.objectByPath("configurations");
661         if (configs) {
662             for (int i = 0; i < configs.length; i++) {
663                 Setting s = configs[i];
664                 if (s) {
665                     string[] paths = s.getStringArray("sourcePaths");
666                     foreach(path; paths)
667                         addRelativePathIfExists(res, path);
668                 }
669             }
670         }
671         if (!res.length) {
672             addRelativePathIfExists(res, "src");
673             addRelativePathIfExists(res, "source");
674         }
675 
676         return res;
677     }
678 
679     void processSubpackages() {
680         import dlangui.core.files;
681         _subPackages.length = 0;
682         Setting subPackages = _projectFile.settingByPath("subPackages", SettingType.ARRAY, false);
683         if (subPackages) {
684             string p = _projectFile.filename.dirName;
685             for(int i = 0; i < subPackages.length; i++) {
686                 Setting sp = subPackages[i];
687                 if (sp.isString) {
688                     // string
689                     string path = convertPathDelimiters(sp.str);
690                     string relative = relativePath(path, p);
691                     path = buildNormalizedPath(absolutePath(relative, p));
692                     //Log.d("Subproject path: ", path);
693                     string fn = DubPackageFinder.findPackageFile(path);
694                     //Log.d("Subproject file: ", fn);
695                     Project prj = new Project(_workspace, fn);
696                     prj.setBaseProject(this);
697                     if (prj.load()) {
698                         Log.d("Loaded subpackage from file: ", fn);
699                         _subPackages ~= prj;
700                         if (_workspace)
701                             _workspace.addDependencyProject(prj);
702                     } else {
703                         Log.w("Failed to load subpackage from file: ", fn);
704                     }
705                 } else if (sp.isObject) {
706                     // object - file inside base project dub.json
707                     Log.d("Subpackage is JSON object");
708                     string subname = sp.getString("name");
709                     if (subname) {
710                         Project prj = new Project(_workspace, _filename ~ "@" ~ subname);
711                         prj.setBaseProject(this);
712                         prj.setSubprojectJson(sp);
713                         bool res = prj.processLoadedProject();
714                         if (res) {
715                             Log.d("Added embedded subpackage ", subname);
716                             _subPackages ~= prj;
717                             if (_workspace)
718                                 _workspace.addDependencyProject(prj);
719                         } else {
720                             Log.w("Error while processing embedded subpackage");
721                         }
722                     }
723                 }
724             }
725         }
726     }
727 
728     /// parse data from _projectFile after loading
729     bool processLoadedProject() {
730         //
731         _mainSourceFile = null;
732         try {
733             _name = toUTF32(_projectFile.getString("name"));
734             _originalName = _name;
735             if (_baseProjectName) {
736                 _name = _baseProjectName ~ ":" ~ _name;
737             }
738             if (_isDependency) {
739                 _name ~= "-"d;
740                 _name ~= toUTF32(_dependencyVersion.startsWith("~") ? _dependencyVersion[1..$] : _dependencyVersion);
741             }
742             _description = toUTF32(_projectFile.getString("description"));
743             Log.d("  project name: ", _name);
744             Log.d("  project description: ", _description);
745 
746             processSubpackages();
747 
748             string[] srcPaths = findSourcePaths();
749             _sourcePaths = null;
750             _items = findItems(srcPaths);
751             findMainSourceFile();
752 
753             Log.i("Project source paths: ", sourcePaths);
754             Log.i("Builder source paths: ", builderSourcePaths);
755             if (!_isDependency)
756                 loadSelections();
757 
758             _configurations = ProjectConfiguration.load(_projectFile);
759             Log.i("Project configurations: ", _configurations);
760 
761 
762         } catch (Exception e) {
763             Log.e("Cannot read project file", e);
764             return false;
765         }
766         _items.loadFile(filename);
767         return true;
768     }
769 
770     override bool load(string fname = null) {
771         if (!_projectFile)
772             _projectFile = new SettingsFile();
773         if (fname.length > 0)
774             filename = fname;
775         if (!_projectFile.load(_filename)) {
776             Log.e("failed to load project from file ", _filename);
777             return false;
778         }
779         Log.d("Reading project from file ", _filename);
780         return processLoadedProject();
781 
782     }
783 
784     override bool save(string fname = null) {
785         if (_isEmbeddedSubproject)
786             return false;
787         if (fname !is null)
788             filename = fname;
789         assert(filename !is null);
790         return _projectFile.save(filename, true);
791     }
792 
793     protected Project[] _dependencies;
794     @property Project[] dependencies() { return _dependencies; }
795 
796     Project findDependencyProject(string filename) {
797         foreach(dep; _dependencies) {
798             if (dep.filename.equal(filename))
799                 return dep;
800         }
801         return null;
802     }
803 
804     bool loadSelections() {
805         Project[] newdeps;
806         _dependencies.length = 0;
807         auto finder = new DubPackageFinder;
808         scope(exit) destroy(finder);
809         SettingsFile selectionsFile = new SettingsFile(buildNormalizedPath(_dir, "dub.selections.json"));
810         if (!selectionsFile.load()) {
811             _dependencies = newdeps;
812             return false;
813         }
814         Setting versions = selectionsFile.objectByPath("versions");
815         if (!versions.isObject) {
816             _dependencies = newdeps;
817             return false;
818         }
819         string[string] versionMap = versions.strMap;
820         foreach(packageName, packageVersion; versionMap) {
821             string fn = finder.findPackage(packageName, packageVersion);
822             Log.d("dependency ", packageName, " ", packageVersion, " : ", fn ? fn : "NOT FOUND");
823             if (fn) {
824                 Project p = findDependencyProject(fn);
825                 if (p) {
826                     Log.d("Found existing dependency project ", fn);
827                     newdeps ~= p;
828                     continue;
829                 }
830                 p = new Project(_workspace, fn, packageVersion);
831                 if (p.load()) {
832                     newdeps ~= p;
833                     if (_workspace)
834                         _workspace.addDependencyProject(p);
835                 } else {
836                     Log.e("cannot load dependency package ", packageName, " ", packageVersion, " from file ", fn);
837                     destroy(p);
838                 }
839             }
840         }
841         _dependencies = newdeps;
842         return true;
843     }
844 }
845 
846 class DubPackageFinder {
847     string systemDubPath;
848     string userDubPath;
849     string tempPath;
850     this() {
851         version(Windows){
852             systemDubPath = buildNormalizedPath(environment.get("ProgramData"), "dub", "packages");
853             userDubPath = buildNormalizedPath(environment.get("APPDATA"), "dub", "packages");
854             tempPath = buildNormalizedPath(environment.get("TEMP"), "dub", "packages");
855         } else version(Posix){
856             systemDubPath = "/var/lib/dub/packages";
857             userDubPath = buildNormalizedPath(environment.get("HOME"), ".dub", "packages");
858             if(!userDubPath.isAbsolute)
859                 userDubPath = buildNormalizedPath(getcwd(), userDubPath);
860             tempPath = "/tmp/packages";
861         }
862     }
863 
864     /// find package file (dub.json, package.json) in specified dir; returns absoulute path to found file or null if not found
865     static string findPackageFile(string pathName) {
866         string fn = buildNormalizedPath(pathName, "dub.json");
867         if (fn.exists && fn.isFile)
868             return fn;
869         fn = buildNormalizedPath(pathName, "dub.sdl");
870         if (fn.exists && fn.isFile)
871             return fn;
872         fn = buildNormalizedPath(pathName, "package.json");
873         if (fn.exists && fn.isFile)
874             return fn;
875         return null;
876     }
877 
878     protected string findPackage(string packageDir, string packageName, string packageVersion) {
879         string fullName = packageVersion.startsWith("~") ? packageName ~ "-" ~ packageVersion[1..$] : packageName ~ "-" ~ packageVersion;
880         string pathName = absolutePath(buildNormalizedPath(packageDir, fullName));
881         if (pathName.exists && pathName.isDir) {
882             string fn = findPackageFile(pathName);
883             if (fn)
884                 return fn;
885             // new DUB support - with package subdirectory
886             fn = findPackageFile(buildNormalizedPath(pathName, packageName));
887             if (fn)
888                 return fn;
889         }
890         return null;
891     }
892 
893     string findPackage(string packageName, string packageVersion) {
894         string res = null;
895         res = findPackage(userDubPath, packageName, packageVersion);
896         if (res)
897             return res;
898         res = findPackage(systemDubPath, packageName, packageVersion);
899         return res;
900     }
901 }
902 
903 bool isValidProjectName(in string s) pure {
904     if (s.empty)
905         return false;
906     return reduce!q{ a && (b == '_' || b == '-' || std.ascii.isAlphaNum(b)) }(true, s);
907 }
908 
909 bool isValidModuleName(in string s) pure {
910     if (s.empty)
911         return false;
912     return reduce!q{ a && (b == '_' || std.ascii.isAlphaNum(b)) }(true, s);
913 }
914 
915 bool isValidFileName(in string s) pure {
916     if (s.empty)
917         return false;
918     return reduce!q{ a && (b == '_' || b == '.' || b == '-' || std.ascii.isAlphaNum(b)) }(true, s);
919 }
920 
921 unittest {
922     assert(!isValidProjectName(""));
923     assert(isValidProjectName("project"));
924     assert(isValidProjectName("cool_project"));
925     assert(isValidProjectName("project-2"));
926     assert(!isValidProjectName("project.png"));
927     assert(!isValidProjectName("[project]"));
928     assert(!isValidProjectName("<project/>"));
929     assert(!isValidModuleName(""));
930     assert(isValidModuleName("module"));
931     assert(isValidModuleName("awesome_module2"));
932     assert(!isValidModuleName("module-2"));
933     assert(!isValidModuleName("module.png"));
934     assert(!isValidModuleName("[module]"));
935     assert(!isValidModuleName("<module>"));
936     assert(!isValidFileName(""));
937     assert(isValidFileName("file"));
938     assert(isValidFileName("file_2"));
939     assert(isValidFileName("file-2"));
940     assert(isValidFileName("file.txt"));
941     assert(!isValidFileName("[file]"));
942     assert(!isValidFileName("<file>"));
943 }
944 
945 class EditorBookmark {
946     string file;
947     string fullFilePath;
948     string projectFilePath;
949     int line;
950     string projectName;
951 }