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 /// return true if filename matches rules for workspace file names
16 bool isProjectFile(in string filename) pure nothrow {
17     return filename.baseName.equal("dub.json") || filename.baseName.equal("package.json");
18 }
19 
20 string toForwardSlashSeparator(in string filename) pure nothrow {
21     char[] res;
22     foreach(ch; filename) {
23         if (ch == '\\')
24             res ~= '/';
25         else
26             res ~= ch;
27     }
28     return cast(string)res;
29 }
30 
31 /// project item
32 class ProjectItem {
33     protected Project _project;
34     protected ProjectItem _parent;
35     protected string _filename;
36     protected dstring _name;
37 
38     this(string filename) {
39         _filename = buildNormalizedPath(filename);
40         _name = toUTF32(baseName(_filename));
41     }
42 
43     this() {
44     }
45 
46     @property ProjectItem parent() { return _parent; }
47 
48     @property Project project() { return _project; }
49 
50     @property void project(Project p) { _project = p; }
51 
52     @property string filename() { return _filename; }
53 
54     @property dstring name() { return _name; }
55 
56     @property string name8() {
57         return _name.toUTF8;
58     }
59 
60     /// returns true if item is folder
61     @property const bool isFolder() { return false; }
62     /// returns child object count
63     @property int childCount() { return 0; }
64     /// returns child item by index
65     ProjectItem child(int index) { return null; }
66 
67     void refresh() {
68     }
69 
70     ProjectSourceFile findSourceFile(string projectFileName, string fullFileName) {
71         if (fullFileName.equal(_filename))
72             return cast(ProjectSourceFile)this;
73         if (project && projectFileName.equal(project.absoluteToRelativePath(_filename)))
74             return cast(ProjectSourceFile)this;
75         return null;
76     }
77 
78     @property bool isDSourceFile() {
79         if (isFolder)
80             return false;
81         return filename.endsWith(".d") || filename.endsWith(".dd") || filename.endsWith(".dd")  || filename.endsWith(".di") || filename.endsWith(".dh") || filename.endsWith(".ddoc");
82     }
83 
84     @property bool isJsonFile() {
85         if (isFolder)
86             return false;
87         return filename.endsWith(".json") || filename.endsWith(".JSON");
88     }
89 
90     @property bool isDMLFile() {
91         if (isFolder)
92             return false;
93         return filename.endsWith(".dml") || filename.endsWith(".DML");
94     }
95 
96     @property bool isXMLFile() {
97         if (isFolder)
98             return false;
99         return filename.endsWith(".xml") || filename.endsWith(".XML");
100     }
101 }
102 
103 /// Project folder
104 class ProjectFolder : ProjectItem {
105     protected ObjectList!ProjectItem _children;
106 
107     this(string filename) {
108         super(filename);
109     }
110 
111     @property override const bool isFolder() {
112         return true;
113     }
114     @property override int childCount() {
115         return _children.count;
116     }
117     /// returns child item by index
118     override ProjectItem child(int index) {
119         return _children[index];
120     }
121     void addChild(ProjectItem item) {
122         _children.add(item);
123         item._parent = this;
124         item._project = _project;
125     }
126     ProjectItem childByPathName(string path) {
127         for (int i = 0; i < _children.count; i++) {
128             if (_children[i].filename.equal(path))
129                 return _children[i];
130         }
131         return null;
132     }
133     ProjectItem childByName(dstring s) {
134         for (int i = 0; i < _children.count; i++) {
135             if (_children[i].name.equal(s))
136                 return _children[i];
137         }
138         return null;
139     }
140 
141     override ProjectSourceFile findSourceFile(string projectFileName, string fullFileName) {
142         for (int i = 0; i < _children.count; i++) {
143             if (ProjectSourceFile res = _children[i].findSourceFile(projectFileName, fullFileName))
144                 return res;
145         }
146         return null;
147     }
148 
149     bool loadDir(string path) {
150         string src = relativeToAbsolutePath(path);
151         if (exists(src) && isDir(src)) {
152             ProjectFolder existing = cast(ProjectFolder)childByPathName(src);
153             if (existing) {
154                 if (existing.isFolder)
155                     existing.loadItems();
156                 return true;
157             }
158             auto dir = new ProjectFolder(src);
159             addChild(dir);
160             Log.d("    added project folder ", src);
161             dir.loadItems();
162             return true;
163         }
164         return false;
165     }
166 
167     bool loadFile(string path) {
168         string src = relativeToAbsolutePath(path);
169         if (exists(src) && isFile(src)) {
170             ProjectItem existing = childByPathName(src);
171             if (existing)
172                 return true;
173             auto f = new ProjectSourceFile(src);
174             addChild(f);
175             Log.d("    added project file ", src);
176             return true;
177         }
178         return false;
179     }
180 
181     void loadItems() {
182         bool[string] loaded;
183         string path = _filename;
184         if (exists(path) && isFile(path))
185             path = dirName(path);
186         foreach(e; dirEntries(path, SpanMode.shallow)) {
187             string fn = baseName(e.name);
188             if (e.isDir) {
189                 loadDir(fn);
190                 loaded[fn] = true;
191             } else if (e.isFile) {
192                 loadFile(fn);
193                 loaded[fn] = true;
194             }
195         }
196         // removing non-reloaded items
197         for (int i = _children.count - 1; i >= 0; i--) {
198             if (!(toUTF8(_children[i].name) in loaded)) {
199                 _children.remove(i);
200             }
201         }
202     }
203 
204     string relativeToAbsolutePath(string path) {
205         if (isAbsolute(path))
206             return path;
207         string fn = _filename;
208         if (exists(fn) && isFile(fn))
209             fn = dirName(fn);
210         return buildNormalizedPath(fn, path);
211     }
212 
213     override void refresh() {
214         loadItems();
215     }
216 }
217 
218 /// Project source file
219 class ProjectSourceFile : ProjectItem {
220     this(string filename) {
221         super(filename);
222     }
223     /// file path relative to project directory
224     @property string projectFilePath() {
225         return project.absoluteToRelativePath(filename);
226     }
227 }
228 
229 class WorkspaceItem {
230     protected string _filename;
231     protected string _dir;
232     protected dstring _name;
233     protected dstring _description;
234 
235     this(string fname = null) {
236         filename = fname;
237     }
238 
239     /// file name of workspace item
240     @property string filename() { return _filename; }
241 
242     /// workspace item directory
243     @property string dir() { return _dir; }
244 
245     /// file name of workspace item
246     @property void filename(string fname) {
247         if (fname.length > 0) {
248             _filename = buildNormalizedPath(fname);
249             _dir = dirName(filename);
250         } else {
251             _filename = null;
252             _dir = null;
253         }
254     }
255 
256     /// name
257     @property dstring name() { return _name; }
258 
259     @property string name8() {
260         return _name.toUTF8;
261     }
262 
263     /// name
264     @property void name(dstring s) {  _name = s; }
265 
266     /// description
267     @property dstring description() { return _description; }
268     /// description
269     @property void description(dstring s) { _description = s; }
270 
271     /// load
272     bool load(string fname) {
273         // override it
274         return false;
275     }
276 
277     bool save(string fname = null) {
278         return false;
279     }
280 }
281 
282 /// detect DMD source paths
283 string[] dmdSourcePaths() {
284     string[] res;
285     version(Windows) {
286         import dlangui.core.files;
287         string dmdPath = findExecutablePath("dmd");
288         if (dmdPath) {
289             string dmdDir = buildNormalizedPath(dirName(dmdPath), "..", "..", "src");
290             res ~= absolutePath(buildNormalizedPath(dmdDir, "druntime", "import"));
291             res ~= absolutePath(buildNormalizedPath(dmdDir, "phobos"));
292         }
293     } else {
294         res ~= "/usr/include/dmd/druntime/import";
295         res ~= "/usr/include/dmd/phobos";
296     }
297     return res;
298 }
299 
300 /// Stores info about project configuration
301 struct ProjectConfiguration {
302     /// name used to build the project
303     string name;
304     /// type, for libraries one can run tests, for apps - execute them
305     Type type;
306     
307     /// How to display default configuration in ui
308     immutable static string DEFAULT_NAME = "default";
309     /// Default project configuration
310     immutable static ProjectConfiguration DEFAULT = ProjectConfiguration(DEFAULT_NAME, Type.Default);
311     
312     /// Type of configuration
313     enum Type {
314         Default,
315         Executable,
316         Library
317     }
318     
319     private static Type parseType(string s)
320     {
321         switch(s)
322         {
323             case "executable": return Type.Executable;
324             case "library": return Type.Library;
325             case "dynamicLibrary": return Type.Library;
326             case "staticLibrary": return Type.Library;
327             default: return Type.Default;
328         }
329     }
330     
331     /// parsing from setting file
332     static ProjectConfiguration[string] load(Setting s)
333     {
334         ProjectConfiguration[string] res = [DEFAULT_NAME: DEFAULT];
335         Setting configs = s.objectByPath("configurations");
336         if(configs is null || configs.type != SettingType.ARRAY) 
337             return res;
338         
339         foreach(conf; configs) {
340             if(!conf.isObject) continue;
341             Type t = Type.Default;
342             if(auto typeName = conf.getString("targetType"))
343                 t = parseType(typeName);
344             if (string confName = conf.getString("name"))
345                 res[confName] = ProjectConfiguration(confName, t);
346         }
347         return res;
348     }
349 }
350 
351 /// DLANGIDE D project
352 class Project : WorkspaceItem {
353     protected Workspace _workspace;
354     protected bool _opened;
355     protected ProjectFolder _items;
356     protected ProjectSourceFile _mainSourceFile;
357     protected SettingsFile _projectFile;
358     protected ProjectSettings _settingsFile;
359     protected bool _isDependency;
360     protected string _dependencyVersion;
361 
362     protected string[] _sourcePaths;
363     protected string[] _builderSourcePaths;
364     protected ProjectConfiguration[string] _configurations;
365 
366     this(Workspace ws, string fname = null, string dependencyVersion = null) {
367         super(fname);
368         _workspace = ws;
369         _items = new ProjectFolder(fname);
370         _dependencyVersion = dependencyVersion;
371         _isDependency = _dependencyVersion.length > 0;
372         _projectFile = new SettingsFile(fname);
373     }
374 
375     @property ProjectSettings settings() {
376         if (!_settingsFile) {
377             _settingsFile = new ProjectSettings(settingsFileName);
378             _settingsFile.updateDefaults();
379             _settingsFile.load();
380             _settingsFile.save();
381         }
382         return _settingsFile;
383     }
384 
385     @property string settingsFileName() {
386         return buildNormalizedPath(dir, toUTF8(name) ~ ".settings");
387     }
388 
389     @property bool isDependency() { return _isDependency; }
390     @property string dependencyVersion() { return _dependencyVersion; }
391 
392     /// returns project configurations
393     @property const(ProjectConfiguration[string]) configurations() const
394     {
395         return _configurations;
396     }
397 
398     /// direct access to project file (json)
399     @property SettingsFile content() { return _projectFile; }
400 
401     /// name
402     override @property dstring name() {
403         return super.name();
404     }
405 
406     /// name
407     override @property void name(dstring s) {
408         super.name(s);
409         _projectFile.setString("name", toUTF8(s));
410     }
411 
412     /// name
413     override @property dstring description() {
414         return super.description();
415     }
416 
417     /// name
418     override @property void description(dstring s) {
419         super.description(s);
420         _projectFile.setString("description", toUTF8(s));
421     }
422 
423     /// returns project's own source paths
424     @property string[] sourcePaths() { return _sourcePaths; }
425     /// returns project's own source paths
426     @property string[] builderSourcePaths() { 
427         if (!_builderSourcePaths) {
428             _builderSourcePaths = dmdSourcePaths();
429         }
430         return _builderSourcePaths; 
431     }
432 
433     ProjectSourceFile findSourceFile(string projectFileName, string fullFileName) {
434         return _items ? _items.findSourceFile(projectFileName, fullFileName) : null;
435     }
436 
437     private static void addUnique(ref string[] dst, string[] items) {
438         foreach(item; items) {
439             if (!canFind(dst, item))
440                 dst ~= item;
441         }
442     }
443     @property string[] importPaths() {
444         string[] res;
445         addUnique(res, sourcePaths);
446         addUnique(res, builderSourcePaths);
447         foreach(dep; _dependencies) {
448             addUnique(res, dep.sourcePaths);
449         }
450         return res;
451     }
452 
453     string relativeToAbsolutePath(string path) {
454         if (isAbsolute(path))
455             return path;
456         return buildNormalizedPath(_dir, path);
457     }
458 
459     string absoluteToRelativePath(string path) {
460         if (!isAbsolute(path))
461             return path;
462         return relativePath(path, _dir);
463     }
464 
465     @property ProjectSourceFile mainSourceFile() { return _mainSourceFile; }
466     @property ProjectFolder items() { return _items; }
467 
468     @property Workspace workspace() { return _workspace; }
469 
470     @property void workspace(Workspace p) { _workspace = p; }
471 
472     @property string defWorkspaceFile() {
473         return buildNormalizedPath(_filename.dirName, toUTF8(name) ~ WORKSPACE_EXTENSION);
474     }
475 
476     @property bool isExecutable() {
477         // TODO: use targetType
478         return true;
479     }
480 
481     /// return executable file name, or null if it's library project or executable is not found
482     @property string executableFileName() {
483         if (!isExecutable)
484             return null;
485         string exename = toUTF8(name);
486         exename = _projectFile.getString("targetName", exename);
487         // TODO: use targetName
488         version (Windows) {
489             exename = exename ~ ".exe";
490         }
491         string targetPath = _projectFile.getString("targetPath", null);
492         string exePath;
493         if (targetPath.length)
494             exePath = buildNormalizedPath(_filename.dirName, targetPath, exename); // int $targetPath directory
495         else
496             exePath = buildNormalizedPath(_filename.dirName, exename); // in project directory
497         return exePath;
498     }
499 
500     /// working directory for running and debugging project
501     @property string workingDirectory() {
502         // TODO: get from settings
503         return _filename.dirName;
504     }
505 
506     /// commandline parameters for running and debugging project
507     @property string runArgs() {
508         // TODO: get from settings
509         return null;
510     }
511 
512     @property bool runInExternalConsole() {
513         // TODO
514         return true;
515     }
516 
517     ProjectFolder findItems(string[] srcPaths) {
518         auto folder = new ProjectFolder(_filename);
519         folder.project = this;
520         string path = relativeToAbsolutePath("src");
521         if (folder.loadDir(path))
522             _sourcePaths ~= path;
523         path = relativeToAbsolutePath("source");
524         if (folder.loadDir(path))
525             _sourcePaths ~= path;
526         foreach(customPath; srcPaths) {
527             path = relativeToAbsolutePath(customPath);
528             foreach(existing; _sourcePaths)
529                 if (path.equal(existing))
530                     continue; // already exists
531             if (folder.loadDir(path))
532                 _sourcePaths ~= path;
533         }
534         return folder;
535     }
536 
537     void refresh() {
538         for (int i = _items._children.count - 1; i >= 0; i--) {
539             if (_items._children[i].isFolder)
540                 _items._children[i].refresh();
541         }
542     }
543 
544     void findMainSourceFile() {
545         string n = toUTF8(name);
546         string[] mainnames = ["app.d", "main.d", n ~ ".d"];
547         foreach(sname; mainnames) {
548             _mainSourceFile = findSourceFileItem(buildNormalizedPath(_dir, "src", sname));
549             if (_mainSourceFile)
550                 break;
551             _mainSourceFile = findSourceFileItem(buildNormalizedPath(_dir, "source", sname));
552             if (_mainSourceFile)
553                 break;
554         }
555     }
556 
557     /// tries to find source file in project, returns found project source file item, or null if not found
558     ProjectSourceFile findSourceFileItem(ProjectItem dir, string filename, bool fullFileName=true) {
559         foreach(i; 0 .. dir.childCount) {
560             ProjectItem item = dir.child(i);
561             if (item.isFolder) {
562                 ProjectSourceFile res = findSourceFileItem(item, filename, fullFileName);
563                 if (res)
564                     return res;
565             } else {
566                 auto res = cast(ProjectSourceFile)item;
567                 if(res)
568                 {
569                     if(fullFileName && res.filename.equal(filename))
570                         return res;
571                     else if (!fullFileName && res.filename.endsWith(filename))
572                         return res;
573                 }
574             }
575         }
576         return null;
577     }
578 
579     ProjectSourceFile findSourceFileItem(string filename, bool fullFileName=true) {
580         return findSourceFileItem(_items, filename, fullFileName);
581     }
582 
583     override bool load(string fname = null) {
584         if (!_projectFile)
585             _projectFile = new SettingsFile();
586         _mainSourceFile = null;
587         if (fname.length > 0)
588             filename = fname;
589         if (!_projectFile.load(_filename)) {
590             Log.e("failed to load project from file ", _filename);
591             return false;
592         }
593         Log.d("Reading project from file ", _filename);
594 
595         try {
596             _name = toUTF32(_projectFile.getString("name"));
597             if (_isDependency) {
598                 _name ~= "-"d;
599                 _name ~= toUTF32(_dependencyVersion.startsWith("~") ? _dependencyVersion[1..$] : _dependencyVersion);
600             }
601             _description = toUTF32(_projectFile.getString("description"));
602             Log.d("  project name: ", _name);
603             Log.d("  project description: ", _description);
604             string[] srcPaths = _projectFile.getStringArray("sourcePaths");
605             _items = findItems(srcPaths);
606             findMainSourceFile();
607 
608             Log.i("Project source paths: ", sourcePaths);
609             Log.i("Builder source paths: ", builderSourcePaths);
610             if (!_isDependency)
611                 loadSelections();
612 
613             _configurations = ProjectConfiguration.load(_projectFile);
614             Log.i("Project configurations: ", _configurations);
615             
616         } catch (Exception e) {
617             Log.e("Cannot read project file", e);
618             return false;
619         }
620         _items.loadFile(filename);
621         return true;
622     }
623 
624     override bool save(string fname = null) {
625         if (fname !is null)
626             filename = fname;
627         assert(filename !is null);
628         return _projectFile.save(filename, true);
629     }
630 
631     protected Project[] _dependencies;
632     @property Project[] dependencies() { return _dependencies; }
633 
634     Project findDependencyProject(string filename) {
635         foreach(dep; _dependencies) {
636             if (dep.filename.equal(filename))
637                 return dep;
638         }
639         return null;
640     }
641 
642     bool loadSelections() {
643         Project[] newdeps;
644         _dependencies.length = 0;
645         auto finder = new DubPackageFinder;
646         scope(exit) destroy(finder);
647         SettingsFile selectionsFile = new SettingsFile(buildNormalizedPath(_dir, "dub.selections.json"));
648         if (!selectionsFile.load()) {
649             _dependencies = newdeps;
650             return false;
651         }
652         Setting versions = selectionsFile.objectByPath("versions");
653         if (!versions.isObject) {
654             _dependencies = newdeps;
655             return false;
656         }
657         string[string] versionMap = versions.strMap;
658         foreach(packageName, packageVersion; versionMap) {
659             string fn = finder.findPackage(packageName, packageVersion);
660             Log.d("dependency ", packageName, " ", packageVersion, " : ", fn ? fn : "NOT FOUND");
661             if (fn) {
662                 Project p = findDependencyProject(fn);
663                 if (p) {
664                     Log.d("Found existing dependency project ", fn);
665                     newdeps ~= p;
666                     continue;
667                 }
668                 p = new Project(_workspace, fn, packageVersion);
669                 if (p.load()) {
670                     newdeps ~= p;
671                     if (_workspace)
672                         _workspace.addDependencyProject(p);
673                 } else {
674                     Log.e("cannot load dependency package ", packageName, " ", packageVersion, " from file ", fn);
675                     destroy(p);
676                 }
677             }
678         }
679         _dependencies = newdeps;
680         return true;
681     }
682 }
683 
684 class DubPackageFinder {
685     string systemDubPath;
686     string userDubPath;
687     string tempPath;
688     this() {
689         version(Windows){
690             systemDubPath = buildNormalizedPath(environment.get("ProgramData"), "dub", "packages");
691             userDubPath = buildNormalizedPath(environment.get("APPDATA"), "dub", "packages");
692             tempPath = buildNormalizedPath(environment.get("TEMP"), "dub", "packages");
693         } else version(Posix){
694             systemDubPath = "/var/lib/dub/packages";
695             userDubPath = buildNormalizedPath(environment.get("HOME"), ".dub", "packages");
696             if(!userDubPath.isAbsolute)
697                 userDubPath = buildNormalizedPath(getcwd(), userDubPath);
698             tempPath = "/tmp/packages";
699         }
700     }
701 
702     protected string findPackage(string packageDir, string packageName, string packageVersion) {
703         string fullName = packageVersion.startsWith("~") ? packageName ~ "-" ~ packageVersion[1..$] : packageName ~ "-" ~ packageVersion;
704         string pathName = absolutePath(buildNormalizedPath(packageDir, fullName));
705         if (pathName.exists && pathName.isDir) {
706             string fn = buildNormalizedPath(pathName, "dub.json");
707             if (fn.exists && fn.isFile)
708                 return fn;
709             fn = buildNormalizedPath(pathName, "package.json");
710             if (fn.exists && fn.isFile)
711                 return fn;
712             // new DUB support - with package subdirectory
713             fn = buildNormalizedPath(pathName, packageName, "dub.json");
714             if (fn.exists && fn.isFile)
715                 return fn;
716             fn = buildNormalizedPath(pathName, packageName, "package.json");
717             if (fn.exists && fn.isFile)
718                 return fn;
719         }
720         return null;
721     }
722 
723     string findPackage(string packageName, string packageVersion) {
724         string res = null;
725         res = findPackage(userDubPath, packageName, packageVersion);
726         if (res)
727             return res;
728         res = findPackage(systemDubPath, packageName, packageVersion);
729         return res;
730     }
731 }
732 
733 bool isValidProjectName(in string s) pure {
734     if (s.empty)
735         return false;
736     return reduce!q{ a && (b == '_' || b == '-' || std.ascii.isAlphaNum(b)) }(true, s);
737 }
738 
739 bool isValidModuleName(in string s) pure {
740     if (s.empty)
741         return false;
742     return reduce!q{ a && (b == '_' || std.ascii.isAlphaNum(b)) }(true, s);
743 }
744 
745 bool isValidFileName(in string s) pure {
746     if (s.empty)
747         return false;
748     return reduce!q{ a && (b == '_' || b == '.' || b == '-' || std.ascii.isAlphaNum(b)) }(true, s);
749 }
750 
751 unittest {
752     assert(!isValidProjectName(""));
753     assert(isValidProjectName("project"));
754     assert(isValidProjectName("cool_project"));
755     assert(isValidProjectName("project-2"));
756     assert(!isValidProjectName("project.png"));
757     assert(!isValidProjectName("[project]"));
758     assert(!isValidProjectName("<project/>"));
759     assert(!isValidModuleName(""));
760     assert(isValidModuleName("module"));
761     assert(isValidModuleName("awesome_module2"));
762     assert(!isValidModuleName("module-2"));
763     assert(!isValidModuleName("module.png"));
764     assert(!isValidModuleName("[module]"));
765     assert(!isValidModuleName("<module>"));
766     assert(!isValidFileName(""));
767     assert(isValidFileName("file"));
768     assert(isValidFileName("file_2"));
769     assert(isValidFileName("file-2"));
770     assert(isValidFileName("file.txt"));
771     assert(!isValidFileName("[file]"));
772     assert(!isValidFileName("<file>"));
773 }
774 
775 class EditorBookmark {
776     string file;
777     string fullFilePath;
778     string projectFilePath;
779     int line;
780     string projectName;
781 }