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 }