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         // TODO: use targetPath
492         string exePath = buildNormalizedPath(_filename.dirName, "bin", exename);
493         return exePath;
494     }
495 
496     /// working directory for running and debugging project
497     @property string workingDirectory() {
498         // TODO: get from settings
499         return _filename.dirName;
500     }
501 
502     /// commandline parameters for running and debugging project
503     @property string runArgs() {
504         // TODO: get from settings
505         return null;
506     }
507 
508     @property bool runInExternalConsole() {
509         // TODO
510         return true;
511     }
512 
513     ProjectFolder findItems(string[] srcPaths) {
514         auto folder = new ProjectFolder(_filename);
515         folder.project = this;
516         string path = relativeToAbsolutePath("src");
517         if (folder.loadDir(path))
518             _sourcePaths ~= path;
519         path = relativeToAbsolutePath("source");
520         if (folder.loadDir(path))
521             _sourcePaths ~= path;
522         foreach(customPath; srcPaths) {
523             path = relativeToAbsolutePath(customPath);
524             foreach(existing; _sourcePaths)
525                 if (path.equal(existing))
526                     continue; // already exists
527             if (folder.loadDir(path))
528                 _sourcePaths ~= path;
529         }
530         return folder;
531     }
532 
533     void refresh() {
534         for (int i = _items._children.count - 1; i >= 0; i--) {
535             if (_items._children[i].isFolder)
536                 _items._children[i].refresh();
537         }
538     }
539 
540     void findMainSourceFile() {
541         string n = toUTF8(name);
542         string[] mainnames = ["app.d", "main.d", n ~ ".d"];
543         foreach(sname; mainnames) {
544             _mainSourceFile = findSourceFileItem(buildNormalizedPath(_dir, "src", sname));
545             if (_mainSourceFile)
546                 break;
547             _mainSourceFile = findSourceFileItem(buildNormalizedPath(_dir, "source", sname));
548             if (_mainSourceFile)
549                 break;
550         }
551     }
552 
553     /// tries to find source file in project, returns found project source file item, or null if not found
554     ProjectSourceFile findSourceFileItem(ProjectItem dir, string filename, bool fullFileName=true) {
555         foreach(i; 0 .. dir.childCount) {
556             ProjectItem item = dir.child(i);
557             if (item.isFolder) {
558                 ProjectSourceFile res = findSourceFileItem(item, filename, fullFileName);
559                 if (res)
560                     return res;
561             } else {
562                 auto res = cast(ProjectSourceFile)item;
563                 if(res)
564                 {
565                     if(fullFileName && res.filename.equal(filename))
566                         return res;
567                     else if (!fullFileName && res.filename.endsWith(filename))
568                         return res;
569                 }
570             }
571         }
572         return null;
573     }
574 
575     ProjectSourceFile findSourceFileItem(string filename, bool fullFileName=true) {
576         return findSourceFileItem(_items, filename, fullFileName);
577     }
578 
579     override bool load(string fname = null) {
580         if (!_projectFile)
581             _projectFile = new SettingsFile();
582         _mainSourceFile = null;
583         if (fname.length > 0)
584             filename = fname;
585         if (!_projectFile.load(_filename)) {
586             Log.e("failed to load project from file ", _filename);
587             return false;
588         }
589         Log.d("Reading project from file ", _filename);
590 
591         try {
592             _name = toUTF32(_projectFile.getString("name"));
593             if (_isDependency) {
594                 _name ~= "-"d;
595                 _name ~= toUTF32(_dependencyVersion.startsWith("~") ? _dependencyVersion[1..$] : _dependencyVersion);
596             }
597             _description = toUTF32(_projectFile.getString("description"));
598             Log.d("  project name: ", _name);
599             Log.d("  project description: ", _description);
600             string[] srcPaths = _projectFile.getStringArray("sourcePaths");
601             _items = findItems(srcPaths);
602             findMainSourceFile();
603 
604             Log.i("Project source paths: ", sourcePaths);
605             Log.i("Builder source paths: ", builderSourcePaths);
606             if (!_isDependency)
607                 loadSelections();
608 
609             _configurations = ProjectConfiguration.load(_projectFile);
610             Log.i("Project configurations: ", _configurations);
611             
612         } catch (Exception e) {
613             Log.e("Cannot read project file", e);
614             return false;
615         }
616         _items.loadFile(filename);
617         return true;
618     }
619 
620     override bool save(string fname = null) {
621         if (fname !is null)
622             filename = fname;
623         assert(filename !is null);
624         return _projectFile.save(filename, true);
625     }
626 
627     protected Project[] _dependencies;
628     @property Project[] dependencies() { return _dependencies; }
629 
630     Project findDependencyProject(string filename) {
631         foreach(dep; _dependencies) {
632             if (dep.filename.equal(filename))
633                 return dep;
634         }
635         return null;
636     }
637 
638     bool loadSelections() {
639         Project[] newdeps;
640         _dependencies.length = 0;
641         auto finder = new DubPackageFinder;
642         scope(exit) destroy(finder);
643         SettingsFile selectionsFile = new SettingsFile(buildNormalizedPath(_dir, "dub.selections.json"));
644         if (!selectionsFile.load()) {
645             _dependencies = newdeps;
646             return false;
647         }
648         Setting versions = selectionsFile.objectByPath("versions");
649         if (!versions.isObject) {
650             _dependencies = newdeps;
651             return false;
652         }
653         string[string] versionMap = versions.strMap;
654         foreach(packageName, packageVersion; versionMap) {
655             string fn = finder.findPackage(packageName, packageVersion);
656             Log.d("dependency ", packageName, " ", packageVersion, " : ", fn ? fn : "NOT FOUND");
657             if (fn) {
658                 Project p = findDependencyProject(fn);
659                 if (p) {
660                     Log.d("Found existing dependency project ", fn);
661                     newdeps ~= p;
662                     continue;
663                 }
664                 p = new Project(_workspace, fn, packageVersion);
665                 if (p.load()) {
666                     newdeps ~= p;
667                     if (_workspace)
668                         _workspace.addDependencyProject(p);
669                 } else {
670                     Log.e("cannot load dependency package ", packageName, " ", packageVersion, " from file ", fn);
671                     destroy(p);
672                 }
673             }
674         }
675         _dependencies = newdeps;
676         return true;
677     }
678 }
679 
680 class DubPackageFinder {
681     string systemDubPath;
682     string userDubPath;
683     string tempPath;
684     this() {
685         version(Windows){
686             systemDubPath = buildNormalizedPath(environment.get("ProgramData"), "dub", "packages");
687             userDubPath = buildNormalizedPath(environment.get("APPDATA"), "dub", "packages");
688             tempPath = buildNormalizedPath(environment.get("TEMP"), "dub", "packages");
689         } else version(Posix){
690             systemDubPath = "/var/lib/dub/packages";
691             userDubPath = buildNormalizedPath(environment.get("HOME"), ".dub", "packages");
692             if(!userDubPath.isAbsolute)
693                 userDubPath = buildNormalizedPath(getcwd(), userDubPath);
694             tempPath = "/tmp/packages";
695         }
696     }
697 
698     protected string findPackage(string packageDir, string packageName, string packageVersion) {
699         string fullName = packageVersion.startsWith("~") ? packageName ~ "-" ~ packageVersion[1..$] : packageName ~ "-" ~ packageVersion;
700         string pathName = absolutePath(buildNormalizedPath(packageDir, fullName));
701         if (pathName.exists && pathName.isDir) {
702             string fn = buildNormalizedPath(pathName, "dub.json");
703             if (fn.exists && fn.isFile)
704                 return fn;
705             fn = buildNormalizedPath(pathName, "package.json");
706             if (fn.exists && fn.isFile)
707                 return fn;
708             // new DUB support - with package subdirectory
709             fn = buildNormalizedPath(pathName, packageName, "dub.json");
710             if (fn.exists && fn.isFile)
711                 return fn;
712             fn = buildNormalizedPath(pathName, packageName, "package.json");
713             if (fn.exists && fn.isFile)
714                 return fn;
715         }
716         return null;
717     }
718 
719     string findPackage(string packageName, string packageVersion) {
720         string res = null;
721         res = findPackage(userDubPath, packageName, packageVersion);
722         if (res)
723             return res;
724         res = findPackage(systemDubPath, packageName, packageVersion);
725         return res;
726     }
727 }
728 
729 bool isValidProjectName(in string s) pure {
730     if (s.empty)
731         return false;
732     return reduce!q{ a && (b == '_' || b == '-' || std.ascii.isAlphaNum(b)) }(true, s);
733 }
734 
735 bool isValidModuleName(in string s) pure {
736     if (s.empty)
737         return false;
738     return reduce!q{ a && (b == '_' || std.ascii.isAlphaNum(b)) }(true, s);
739 }
740 
741 bool isValidFileName(in string s) pure {
742     if (s.empty)
743         return false;
744     return reduce!q{ a && (b == '_' || b == '.' || b == '-' || std.ascii.isAlphaNum(b)) }(true, s);
745 }
746 
747 unittest {
748     assert(!isValidProjectName(""));
749     assert(isValidProjectName("project"));
750     assert(isValidProjectName("cool_project"));
751     assert(isValidProjectName("project-2"));
752     assert(!isValidProjectName("project.png"));
753     assert(!isValidProjectName("[project]"));
754     assert(!isValidProjectName("<project/>"));
755     assert(!isValidModuleName(""));
756     assert(isValidModuleName("module"));
757     assert(isValidModuleName("awesome_module2"));
758     assert(!isValidModuleName("module-2"));
759     assert(!isValidModuleName("module.png"));
760     assert(!isValidModuleName("[module]"));
761     assert(!isValidModuleName("<module>"));
762     assert(!isValidFileName(""));
763     assert(isValidFileName("file"));
764     assert(isValidFileName("file_2"));
765     assert(isValidFileName("file-2"));
766     assert(isValidFileName("file.txt"));
767     assert(!isValidFileName("[file]"));
768     assert(!isValidFileName("<file>"));
769 }
770 
771 class EditorBookmark {
772     string file;
773     string fullFilePath;
774     string projectFilePath;
775     int line;
776     string projectName;
777 }