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