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 }