1 module workspaced.api; 2 3 // debug = Tasks; 4 5 import core.time; 6 import dparse.lexer; 7 import painlessjson; 8 import standardpaths; 9 10 import std.algorithm; 11 import std.conv; 12 import std.exception; 13 import std.file; 14 import std.json; 15 import std.meta; 16 import std.parallelism; 17 import std.path; 18 import std.range; 19 import std.regex; 20 import std.traits; 21 import std.typecons; 22 23 /// 24 alias ImportPathProvider = string[] delegate() nothrow; 25 /// 26 alias BroadcastCallback = void delegate(WorkspaceD, WorkspaceD.Instance, JSONValue); 27 /// 28 alias ComponentBindFailCallback = void delegate(WorkspaceD.Instance, ComponentFactory, Exception); 29 30 /// Will never call this function 31 enum ignoredFunc; 32 33 /// Component call 34 struct ComponentInfo 35 { 36 /// Name of the component 37 string name; 38 } 39 40 ComponentInfo component(string name) 41 { 42 return ComponentInfo(name); 43 } 44 45 void traceTaskLog(lazy string msg) 46 { 47 import std.stdio : stderr; 48 49 debug (Tasks) 50 stderr.writeln(msg); 51 } 52 53 static immutable traceTask = `traceTaskLog("new task in " ~ __PRETTY_FUNCTION__); scope (exit) traceTaskLog(__PRETTY_FUNCTION__ ~ " exited");`; 54 55 mixin template DefaultComponentWrapper(bool withDtor = true) 56 { 57 @ignoredFunc 58 { 59 import std.algorithm : min, max; 60 import std.parallelism : TaskPool, Task, task, defaultPoolThreads; 61 62 WorkspaceD workspaced; 63 WorkspaceD.Instance refInstance; 64 65 TaskPool _threads; 66 67 static if (withDtor) 68 { 69 ~this() 70 { 71 shutdown(); 72 } 73 } 74 75 TaskPool gthreads() 76 { 77 return workspaced.gthreads; 78 } 79 80 TaskPool threads(int minSize, int maxSize) 81 { 82 if (!_threads) 83 synchronized (this) 84 if (!_threads) 85 _threads = new TaskPool(max(minSize, min(maxSize, defaultPoolThreads))); 86 return _threads; 87 } 88 89 WorkspaceD.Instance instance() const @property 90 { 91 if (refInstance) 92 return cast() refInstance; 93 else 94 throw new Exception("Attempted to access instance in a global context"); 95 } 96 97 WorkspaceD.Instance instance(WorkspaceD.Instance instance) @property 98 { 99 return refInstance = instance; 100 } 101 102 string[] importPaths() const @property 103 { 104 return instance.importPathProvider ? instance.importPathProvider() : []; 105 } 106 107 string[] stringImportPaths() const @property 108 { 109 return instance.stringImportPathProvider ? instance.stringImportPathProvider() : []; 110 } 111 112 string[] importFiles() const @property 113 { 114 return instance.importFilesProvider ? instance.importFilesProvider() : []; 115 } 116 117 ref ImportPathProvider importPathProvider() @property 118 { 119 return instance.importPathProvider; 120 } 121 122 ref ImportPathProvider stringImportPathProvider() @property 123 { 124 return instance.stringImportPathProvider; 125 } 126 127 ref ImportPathProvider importFilesProvider() @property 128 { 129 return instance.importFilesProvider; 130 } 131 132 ref Configuration config() @property 133 { 134 if (refInstance) 135 return refInstance.config; 136 else if (workspaced) 137 return workspaced.globalConfiguration; 138 else 139 assert(false, "Unbound component trying to access config."); 140 } 141 142 T get(T)() 143 { 144 if (refInstance) 145 return refInstance.get!T; 146 else if (workspaced) 147 return workspaced.get!T; 148 else 149 assert(false, "Unbound component trying to get component " ~ T.stringof ~ "."); 150 } 151 152 string cwd() @property const 153 { 154 return instance.cwd; 155 } 156 157 override void shutdown() 158 { 159 if (_threads) 160 _threads.finish(); 161 } 162 163 override void bind(WorkspaceD workspaced, WorkspaceD.Instance instance) 164 { 165 this.workspaced = workspaced; 166 this.instance = instance; 167 static if (__traits(hasMember, typeof(this).init, "load")) 168 load(); 169 } 170 171 import std.conv; 172 import std.json : JSONValue; 173 import std.traits : isFunction, hasUDA, ParameterDefaults, Parameters, ReturnType; 174 import painlessjson; 175 176 override Future!JSONValue run(string method, JSONValue[] args) 177 { 178 static foreach (member; __traits(derivedMembers, typeof(this))) 179 static if (member[0] != '_' && __traits(compiles, __traits(getMember, 180 typeof(this).init, member)) && __traits(getProtection, __traits(getMember, typeof(this).init, 181 member)) == "public" && __traits(compiles, isFunction!(__traits(getMember, 182 typeof(this).init, member))) && isFunction!(__traits(getMember, 183 typeof(this).init, member)) && !hasUDA!(__traits(getMember, typeof(this).init, 184 member), ignoredFunc) && !__traits(isTemplate, __traits(getMember, 185 typeof(this).init, member))) 186 if (method == member) 187 return runMethod!member(args); 188 throw new Exception("Method " ~ method ~ " not found."); 189 } 190 191 Future!JSONValue runMethod(string method)(JSONValue[] args) 192 { 193 int matches; 194 static foreach (overload; __traits(getOverloads, typeof(this), method)) 195 { 196 if (matchesOverload!overload(args)) 197 matches++; 198 } 199 if (matches == 0) 200 throw new Exception("No suitable overload found for " ~ method ~ "."); 201 if (matches > 1) 202 throw new Exception("Multiple overloads found for " ~ method ~ "."); 203 static foreach (overload; __traits(getOverloads, typeof(this), method)) 204 { 205 if (matchesOverload!overload(args)) 206 return runOverload!overload(args); 207 } 208 assert(false); 209 } 210 211 Future!JSONValue runOverload(alias fun)(JSONValue[] args) 212 { 213 mixin(generateOverloadCall!fun); 214 } 215 216 static string generateOverloadCall(alias fun)() 217 { 218 string call = "fun("; 219 static foreach (i, T; Parameters!fun) 220 { 221 static if (is(T : const(char)[])) 222 call ~= "args[" ~ i.to!string ~ "].str, "; 223 else 224 call ~= "args[" ~ i.to!string ~ "].fromJSON!(" ~ T.stringof ~ "), "; 225 } 226 call ~= ")"; 227 static if (is(ReturnType!fun : Future!T, T)) 228 { 229 static if (is(T == void)) 230 string conv = "ret.finish(JSONValue(null));"; 231 else 232 string conv = "ret.finish(v.value.toJSON);"; 233 return "auto ret = new Future!JSONValue; auto v = " ~ call 234 ~ "; v.onDone = { if (v.exception) ret.error(v.exception); else " 235 ~ conv ~ " }; return ret;"; 236 } 237 else static if (is(ReturnType!fun == void)) 238 return call ~ "; return Future!JSONValue.fromResult(JSONValue(null));"; 239 else 240 return "return Future!JSONValue.fromResult(" ~ call ~ ".toJSON);"; 241 } 242 } 243 } 244 245 bool matchesOverload(alias fun)(JSONValue[] args) 246 { 247 if (args.length > Parameters!fun.length) 248 return false; 249 static foreach (i, def; ParameterDefaults!fun) 250 { 251 static if (is(def == void)) 252 { 253 if (i >= args.length) 254 return false; 255 else if (!checkType!(Parameters!fun[i])(args[i])) 256 return false; 257 } 258 } 259 return true; 260 } 261 262 bool checkType(T)(JSONValue value) 263 { 264 final switch (value.type) 265 { 266 case JSON_TYPE.ARRAY: 267 static if (isStaticArray!T) 268 return T.length == value.array.length 269 && value.array.all!(checkType!(typeof(T.init[0]))); 270 else static if (isDynamicArray!T) 271 return value.array.all!(checkType!(typeof(T.init[0]))); 272 else static if (is(T : Tuple!Args, Args...)) 273 { 274 if (value.array.length != Args.length) 275 return false; 276 static foreach (i, Arg; Args) 277 if (!checkType!Arg(value.array[i])) 278 return false; 279 return true; 280 } 281 else 282 return false; 283 case JSON_TYPE.FALSE: 284 case JSON_TYPE.TRUE: 285 return is(T : bool); 286 case JSON_TYPE.FLOAT: 287 return isNumeric!T; 288 case JSON_TYPE.INTEGER: 289 case JSON_TYPE.UINTEGER: 290 return isIntegral!T; 291 case JSON_TYPE.NULL: 292 static if (is(T == class) || isArray!T || isPointer!T || is(T : Nullable!U, U)) 293 return true; 294 else 295 return false; 296 case JSON_TYPE.OBJECT: 297 return is(T == class) || is(T == struct); 298 case JSON_TYPE.STRING: 299 return isSomeString!T; 300 } 301 } 302 303 interface ComponentWrapper 304 { 305 void bind(WorkspaceD workspaced, WorkspaceD.Instance instance); 306 Future!JSONValue run(string method, JSONValue[] args); 307 void shutdown(); 308 } 309 310 interface ComponentFactory 311 { 312 ComponentWrapper create(WorkspaceD workspaced, WorkspaceD.Instance instance, out Exception error); 313 ComponentInfo info() @property; 314 } 315 316 struct ComponentFactoryInstance 317 { 318 ComponentFactory factory; 319 bool autoRegister; 320 alias factory this; 321 } 322 323 struct ComponentWrapperInstance 324 { 325 ComponentWrapper wrapper; 326 ComponentInfo info; 327 } 328 329 struct Configuration 330 { 331 /// JSON containing base configuration formatted as {[component]:{key:value pairs}} 332 JSONValue base; 333 334 bool get(string component, string key, out JSONValue val) 335 { 336 if (base.type != JSON_TYPE.OBJECT) 337 { 338 JSONValue[string] tmp; 339 base = JSONValue(tmp); 340 } 341 auto com = component in base.object; 342 if (!com) 343 return false; 344 auto v = key in *com; 345 if (!v) 346 return false; 347 val = *v; 348 return true; 349 } 350 351 T get(T)(string component, string key, T defaultValue = T.init) 352 { 353 JSONValue ret; 354 if (!get(component, key, ret)) 355 return defaultValue; 356 return ret.fromJSON!T; 357 } 358 359 bool set(T)(string component, string key, T value) 360 { 361 if (base.type != JSON_TYPE.OBJECT) 362 { 363 JSONValue[string] tmp; 364 base = JSONValue(tmp); 365 } 366 auto com = component in base.object; 367 if (!com) 368 { 369 JSONValue[string] val; 370 val[key] = value.toJSON; 371 base.object[component] = JSONValue(val); 372 } 373 else 374 { 375 com.object[key] = value.toJSON; 376 } 377 return true; 378 } 379 380 /// Same as init but might make nicer code. 381 static immutable Configuration none = Configuration.init; 382 383 /// Loads unset keys from global, keeps existing keys 384 void loadBase(Configuration global) 385 { 386 if (global.base.type != JSON_TYPE.OBJECT) 387 return; 388 389 if (base.type != JSON_TYPE.OBJECT) 390 base = global.base.dupJson; 391 else 392 { 393 foreach (component, config; global.base.object) 394 { 395 auto existing = component in base.object; 396 if (!existing || config.type != JSON_TYPE.OBJECT) 397 base.object[component] = config.dupJson; 398 else 399 { 400 foreach (key, value; config.object) 401 { 402 auto existingValue = key in *existing; 403 if (!existingValue) 404 (*existing)[key] = value.dupJson; 405 } 406 } 407 } 408 } 409 } 410 } 411 412 private JSONValue dupJson(JSONValue v) 413 { 414 switch (v.type) 415 { 416 case JSON_TYPE.OBJECT: 417 return JSONValue(v.object.dup); 418 case JSON_TYPE.ARRAY: 419 return JSONValue(v.array.dup); 420 default: 421 return v; 422 } 423 } 424 425 /// WorkspaceD instance holding plugins. 426 class WorkspaceD 427 { 428 static class Instance 429 { 430 string cwd; 431 ComponentWrapperInstance[] instanceComponents; 432 Configuration config; 433 434 string[] importPaths() const @property nothrow 435 { 436 return importPathProvider ? importPathProvider() : []; 437 } 438 439 string[] stringImportPaths() const @property nothrow 440 { 441 return stringImportPathProvider ? stringImportPathProvider() : []; 442 } 443 444 string[] importFiles() const @property nothrow 445 { 446 return importFilesProvider ? importFilesProvider() : []; 447 } 448 449 void shutdown() 450 { 451 foreach (ref com; instanceComponents) 452 com.wrapper.shutdown(); 453 instanceComponents = null; 454 } 455 456 ImportPathProvider importPathProvider; 457 ImportPathProvider stringImportPathProvider; 458 ImportPathProvider importFilesProvider; 459 460 Future!JSONValue run(WorkspaceD workspaced, string component, string method, JSONValue[] args) 461 { 462 foreach (ref com; instanceComponents) 463 if (com.info.name == component) 464 return com.wrapper.run(method, args); 465 throw new Exception("Component '" ~ component ~ "' not found"); 466 } 467 468 T get(T)() 469 { 470 auto name = getUDAs!(T, ComponentInfo)[0].name; 471 foreach (com; instanceComponents) 472 if (com.info.name == name) 473 return cast(T) com.wrapper; 474 throw new Exception( 475 "Attempted to get unknown instance component " ~ T.stringof ~ " in instance cwd:" ~ cwd); 476 } 477 478 bool has(T)() 479 { 480 auto name = getUDAs!(T, ComponentInfo)[0].name; 481 foreach (com; instanceComponents) 482 if (com.info.name == name) 483 return true; 484 return false; 485 } 486 487 /// Loads a registered component which didn't have auto register on just for this instance. 488 /// Returns: false instead of using the onBindFail callback on failure. 489 /// Throws: Exception if component was not registered in workspaced. 490 bool attach(T)(WorkspaceD workspaced) 491 { 492 string name = getUDAs!(T, ComponentInfo)[0].name; 493 foreach (factory; workspaced.components) 494 { 495 if (factory.info.name == name) 496 { 497 auto inst = factory.create(workspaced, this); 498 if (inst) 499 { 500 instanceComponents ~= ComponentWrapperInstance(inst, info); 501 return true; 502 } 503 else 504 return false; 505 } 506 } 507 throw new Exception("Component not found"); 508 } 509 } 510 511 BroadcastCallback onBroadcast; 512 ComponentBindFailCallback onBindFail; 513 514 Instance[] instances; 515 /// Base global configuration for new instances, does not modify existing ones. 516 Configuration globalConfiguration; 517 ComponentWrapperInstance[] globalComponents; 518 ComponentFactoryInstance[] components; 519 StringCache stringCache; 520 521 TaskPool _gthreads; 522 523 this() 524 { 525 stringCache = StringCache(StringCache.defaultBucketCount * 4); 526 } 527 528 ~this() 529 { 530 shutdown(); 531 } 532 533 void shutdown() 534 { 535 foreach (ref instance; instances) 536 instance.shutdown(); 537 instances = null; 538 foreach (ref com; globalComponents) 539 com.wrapper.shutdown(); 540 globalComponents = null; 541 components = null; 542 if (_gthreads) 543 _gthreads.finish(true); 544 _gthreads = null; 545 } 546 547 void broadcast(WorkspaceD.Instance instance, JSONValue value) 548 { 549 if (onBroadcast) 550 onBroadcast(this, instance, value); 551 } 552 553 Instance getInstance(string cwd) nothrow 554 { 555 cwd = buildNormalizedPath(cwd); 556 foreach (instance; instances) 557 if (instance.cwd == cwd) 558 return instance; 559 return null; 560 } 561 562 Instance getBestInstanceByDependency(WithComponent)(string file) nothrow 563 { 564 Instance best; 565 size_t bestLength; 566 foreach (instance; instances) 567 { 568 foreach (folder; chain(instance.importPaths, instance.importFiles, 569 instance.stringImportPaths)) 570 { 571 if (folder.length > bestLength && file.startsWith(folder) && instance.has!WithComponent) 572 { 573 best = instance; 574 bestLength = folder.length; 575 } 576 } 577 } 578 return best; 579 } 580 581 Instance getBestInstanceByDependency(string file) nothrow 582 { 583 Instance best; 584 size_t bestLength; 585 foreach (instance; instances) 586 { 587 foreach (folder; chain(instance.importPaths, instance.importFiles, 588 instance.stringImportPaths)) 589 { 590 if (folder.length > bestLength && file.startsWith(folder)) 591 { 592 best = instance; 593 bestLength = folder.length; 594 } 595 } 596 } 597 return best; 598 } 599 600 Instance getBestInstance(WithComponent)(string file, bool fallback = true) nothrow 601 { 602 file = buildNormalizedPath(file); 603 Instance ret = null; 604 size_t best; 605 foreach (instance; instances) 606 { 607 if (instance.cwd.length > best && file.startsWith(instance.cwd) && instance 608 .has!WithComponent) 609 { 610 ret = instance; 611 best = instance.cwd.length; 612 } 613 } 614 if (!ret && fallback) 615 { 616 ret = getBestInstanceByDependency!WithComponent(file); 617 if (ret) 618 return ret; 619 foreach (instance; instances) 620 if (instance.has!WithComponent) 621 return instance; 622 } 623 return ret; 624 } 625 626 Instance getBestInstance(string file, bool fallback = true) nothrow 627 { 628 file = buildNormalizedPath(file); 629 Instance ret = null; 630 size_t best; 631 foreach (instance; instances) 632 { 633 if (instance.cwd.length > best && file.startsWith(instance.cwd)) 634 { 635 ret = instance; 636 best = instance.cwd.length; 637 } 638 } 639 if (!ret && fallback && instances.length) 640 { 641 ret = getBestInstanceByDependency(file); 642 if (!ret) 643 ret = instances[0]; 644 } 645 return ret; 646 } 647 648 T get(T)() 649 { 650 auto name = getUDAs!(T, ComponentInfo)[0].name; 651 foreach (com; globalComponents) 652 if (com.info.name == name) 653 return cast(T) com.wrapper; 654 throw new Exception("Attempted to get unknown global component " ~ T.stringof); 655 } 656 657 bool has(T)() 658 { 659 auto name = getUDAs!(T, ComponentInfo)[0].name; 660 foreach (com; globalComponents) 661 if (com.info.name == name) 662 return true; 663 return false; 664 } 665 666 T get(T)(string cwd) 667 { 668 if (!cwd.length) 669 return this.get!T; 670 auto inst = getInstance(cwd); 671 if (inst is null) 672 throw new Exception("cwd '" ~ cwd ~ "' not found"); 673 return inst.get!T; 674 } 675 676 bool has(T)(string cwd) 677 { 678 auto inst = getInstance(cwd); 679 if (inst is null) 680 return false; 681 return inst.has!T; 682 } 683 684 T best(T)(string file, bool fallback = true) 685 { 686 if (!file.length) 687 return this.get!T; 688 auto inst = getBestInstance!T(file); 689 if (inst is null) 690 throw new Exception("cwd for '" ~ file ~ "' not found"); 691 return inst.get!T; 692 } 693 694 bool hasBest(T)(string cwd, bool fallback = true) 695 { 696 auto inst = getBestInstance!T(cwd); 697 if (inst is null) 698 return false; 699 return inst.has!T; 700 } 701 702 Future!JSONValue run(string cwd, string component, string method, JSONValue[] args) 703 { 704 auto instance = getInstance(cwd); 705 if (instance is null) 706 throw new Exception("cwd '" ~ cwd ~ "' not found"); 707 return instance.run(this, component, method, args); 708 } 709 710 Future!JSONValue run(string component, string method, JSONValue[] args) 711 { 712 foreach (ref com; globalComponents) 713 if (com.info.name == component) 714 return com.wrapper.run(method, args); 715 throw new Exception("Global component '" ~ component ~ "' not found"); 716 } 717 718 ComponentFactory register(T)(bool autoRegister = true) 719 { 720 ComponentFactory factory; 721 static foreach (attr; __traits(getAttributes, T)) 722 static if (is(attr == class) && is(attr : ComponentFactory)) 723 factory = new attr; 724 if (factory is null) 725 factory = new DefaultComponentFactory!T; 726 components ~= ComponentFactoryInstance(factory, autoRegister); 727 auto info = factory.info; 728 Exception error; 729 auto glob = factory.create(this, null, error); 730 if (glob) 731 globalComponents ~= ComponentWrapperInstance(glob, info); 732 if (autoRegister) 733 foreach (ref instance; instances) 734 { 735 auto inst = factory.create(this, instance, error); 736 if (inst) 737 instance.instanceComponents ~= ComponentWrapperInstance(inst, info); 738 else if (onBindFail) 739 onBindFail(instance, factory, error); 740 } 741 static if (__traits(compiles, T.registered(this))) 742 T.registered(this); 743 else static if (__traits(compiles, T.registered())) 744 T.registered(); 745 return factory; 746 } 747 748 /// Creates a new workspace with the given cwd with optional config overrides and preload components for non-autoRegister components. 749 /// Throws: Exception if normalized cwd already exists as instance. 750 Instance addInstance(string cwd, 751 Configuration configOverrides = Configuration.none, string[] preloadComponents = []) 752 { 753 cwd = buildNormalizedPath(cwd); 754 if (instances.canFind!(a => a.cwd == cwd)) 755 throw new Exception("Instance with cwd '" ~ cwd ~ "' already exists!"); 756 auto inst = new Instance(); 757 inst.cwd = cwd; 758 configOverrides.loadBase(globalConfiguration); 759 inst.config = configOverrides; 760 instances ~= inst; 761 foreach (name; preloadComponents) 762 { 763 foreach (factory; components) 764 { 765 if (!factory.autoRegister && factory.info.name == name) 766 { 767 Exception error; 768 auto wrap = factory.create(this, inst, error); 769 if (wrap) 770 inst.instanceComponents ~= ComponentWrapperInstance(wrap, factory.info); 771 else if (onBindFail) 772 onBindFail(inst, factory, error); 773 break; 774 } 775 } 776 } 777 foreach (factory; components) 778 { 779 if (factory.autoRegister) 780 { 781 Exception error; 782 auto wrap = factory.create(this, inst, error); 783 if (wrap) 784 inst.instanceComponents ~= ComponentWrapperInstance(wrap, factory.info); 785 else if (onBindFail) 786 onBindFail(inst, factory, error); 787 } 788 } 789 return inst; 790 } 791 792 bool removeInstance(string cwd) 793 { 794 cwd = buildNormalizedPath(cwd); 795 foreach (i, instance; instances) 796 if (instance.cwd == cwd) 797 { 798 foreach (com; instance.instanceComponents) 799 destroy(com.wrapper); 800 destroy(instance); 801 instances = instances.remove(i); 802 return true; 803 } 804 return false; 805 } 806 807 deprecated("Use overload taking an out Exception error or attachSilent instead") bool attach( 808 Instance instance, string component) 809 { 810 return attachSilent(instance, component); 811 } 812 813 bool attachSilent(Instance instance, string component) 814 { 815 Exception error; 816 return attach(instance, component, error); 817 } 818 819 bool attach(Instance instance, string component, out Exception error) 820 { 821 foreach (factory; components) 822 { 823 if (factory.info.name == component) 824 { 825 auto wrap = factory.create(this, instance, error); 826 if (wrap) 827 { 828 instance.instanceComponents ~= ComponentWrapperInstance(wrap, factory.info); 829 return true; 830 } 831 else 832 return false; 833 } 834 } 835 return false; 836 } 837 838 TaskPool gthreads() 839 { 840 if (!_gthreads) 841 synchronized (this) 842 if (!_gthreads) 843 _gthreads = new TaskPool(max(2, min(6, defaultPoolThreads))); 844 return _gthreads; 845 } 846 } 847 848 class DefaultComponentFactory(T : ComponentWrapper) : ComponentFactory 849 { 850 ComponentWrapper create(WorkspaceD workspaced, WorkspaceD.Instance instance, out Exception error) 851 { 852 auto wrapper = new T(); 853 try 854 { 855 wrapper.bind(workspaced, instance); 856 return wrapper; 857 } 858 catch (Exception e) 859 { 860 error = e; 861 return null; 862 } 863 } 864 865 ComponentInfo info() @property 866 { 867 alias udas = getUDAs!(T, ComponentInfo); 868 static assert(udas.length == 1, "Can't construct default component factory for " 869 ~ T.stringof ~ ", expected exactly 1 ComponentInfo instance attached to the type"); 870 return udas[0]; 871 } 872 } 873 874 /// Describes what to insert/replace/delete to do something 875 struct CodeReplacement 876 { 877 /// Range what to replace. If both indices are the same its inserting. 878 size_t[2] range; 879 /// Content to replace it with. Empty means remove. 880 string content; 881 882 /// Applies this edit to a string. 883 string apply(string code) 884 { 885 size_t min = range[0]; 886 size_t max = range[1]; 887 if (min > max) 888 { 889 min = range[1]; 890 max = range[0]; 891 } 892 if (min >= code.length) 893 return code ~ content; 894 if (max >= code.length) 895 return code[0 .. min] ~ content; 896 return code[0 .. min] ~ content ~ code[max .. $]; 897 } 898 } 899 900 /// Code replacements mapped to a file 901 struct FileChanges 902 { 903 /// File path to change. 904 string file; 905 /// Replacements to apply. 906 CodeReplacement[] replacements; 907 } 908 909 package bool getConfigPath(string file, ref string retPath) 910 { 911 foreach (dir; standardPaths(StandardPath.config, "workspace-d")) 912 { 913 auto path = buildPath(dir, file); 914 if (path.exists) 915 { 916 retPath = path; 917 return true; 918 } 919 } 920 return false; 921 } 922 923 enum verRegex = ctRegex!`(\d+)\.(\d+)\.(\d+)`; 924 bool checkVersion(string ver, int[3] target) 925 { 926 auto match = ver.matchFirst(verRegex); 927 if (!match) 928 return false; 929 int major = match[1].to!int; 930 int minor = match[2].to!int; 931 int patch = match[3].to!int; 932 if (major > target[0]) 933 return true; 934 if (major == target[0] && minor > target[1]) 935 return true; 936 if (major == target[0] && minor == target[1] && patch >= target[2]) 937 return true; 938 return false; 939 } 940 941 package string getVersionAndFixPath(ref string execPath) 942 { 943 import std.process; 944 945 try 946 { 947 return execute([execPath, "--version"]).output; 948 } 949 catch (ProcessException e) 950 { 951 auto newPath = buildPath(thisExePath.dirName, execPath.baseName); 952 if (exists(newPath)) 953 { 954 execPath = newPath; 955 return execute([execPath, "--version"]).output; 956 } 957 throw e; 958 } 959 } 960 961 class Future(T) 962 { 963 static if (!is(T == void)) 964 T value; 965 Throwable exception; 966 bool has; 967 void delegate() _onDone; 968 969 /// Sets the onDone callback if no value has been set yet or calls immediately if the value has already been set or was set during setting the callback. 970 /// Crashes with an assert error if attempting to override an existing callback (i.e. calling this function on the same object twice). 971 void onDone(void delegate() callback) @property 972 { 973 assert(!_onDone); 974 if (has) 975 callback(); 976 else 977 { 978 bool called; 979 _onDone = { called = true; callback(); }; 980 if (has && !called) 981 callback(); 982 } 983 } 984 985 static if (is(T == void)) 986 static Future!void finished() 987 { 988 auto ret = new Future!void; 989 ret.has = true; 990 return ret; 991 } 992 else 993 static Future!T fromResult(T value) 994 { 995 auto ret = new Future!T; 996 ret.value = value; 997 ret.has = true; 998 return ret; 999 } 1000 1001 static Future!T async(T delegate() cb) 1002 { 1003 import core.thread : Thread; 1004 1005 auto ret = new Future!T; 1006 new Thread({ 1007 try 1008 { 1009 static if (is(T == void)) 1010 { 1011 cb(); 1012 ret.finish(); 1013 } 1014 else 1015 ret.finish(cb()); 1016 } 1017 catch (Throwable t) 1018 { 1019 ret.error(t); 1020 } 1021 }).start(); 1022 return ret; 1023 } 1024 1025 static Future!T fromError(T)(Throwable error) 1026 { 1027 auto ret = new Future!T; 1028 ret.error = error; 1029 ret.has = true; 1030 return ret; 1031 } 1032 1033 static if (is(T == void)) 1034 void finish() 1035 { 1036 assert(!has); 1037 has = true; 1038 if (_onDone) 1039 _onDone(); 1040 } 1041 else 1042 void finish(T value) 1043 { 1044 assert(!has); 1045 this.value = value; 1046 has = true; 1047 if (_onDone) 1048 _onDone(); 1049 } 1050 1051 void error(Throwable t) 1052 { 1053 assert(!has); 1054 exception = t; 1055 has = true; 1056 if (_onDone) 1057 _onDone(); 1058 } 1059 1060 /// Waits for the result of this future using Thread.sleep 1061 T getBlocking(alias sleepDur = 1.msecs)() 1062 { 1063 import core.thread : Thread; 1064 1065 while (!has) 1066 Thread.sleep(sleepDur); 1067 if (exception) 1068 throw exception; 1069 static if (!is(T == void)) 1070 return value; 1071 } 1072 1073 /// Waits for the result of this future using Fiber.yield 1074 T getYield() 1075 { 1076 import core.thread : Fiber; 1077 1078 while (!has) 1079 Fiber.yield(); 1080 if (exception) 1081 throw exception; 1082 static if (!is(T == void)) 1083 return value; 1084 } 1085 } 1086 1087 version (unittest) 1088 { 1089 struct TestingWorkspace 1090 { 1091 string directory; 1092 1093 @disable this(this); 1094 1095 this(string path) 1096 { 1097 if (path.exists) 1098 throw new Exception("Path already exists"); 1099 directory = path; 1100 mkdir(path); 1101 } 1102 1103 ~this() 1104 { 1105 rmdirRecurse(directory); 1106 } 1107 1108 string getPath(string path) 1109 { 1110 return buildPath(directory, path); 1111 } 1112 1113 void createDir(string dir) 1114 { 1115 mkdirRecurse(getPath(dir)); 1116 } 1117 1118 void writeFile(string path, string content) 1119 { 1120 write(getPath(path), content); 1121 } 1122 } 1123 1124 TestingWorkspace makeTemporaryTestingWorkspace() 1125 { 1126 import std.random; 1127 1128 return TestingWorkspace(buildPath(tempDir, "workspace-d-test-" ~ uniform(0, 1129 int.max).to!string(36))); 1130 } 1131 } 1132 1133 void create(T)(TaskPool pool, T fun) if (isCallable!T) 1134 { 1135 pool.put(task(fun)); 1136 }