1 module workspaced.com.dcdext; 2 3 import dparse.ast; 4 import dparse.lexer; 5 import dparse.parser; 6 import dparse.rollback_allocator; 7 8 import core.thread; 9 10 import std.algorithm; 11 import std.array; 12 import std.ascii; 13 import std.conv; 14 import std.file; 15 import std.functional; 16 import std.json; 17 import std.range; 18 import std.string; 19 20 import workspaced.api; 21 import workspaced.dparseext; 22 import workspaced.com.dcd; 23 24 import workspaced.visitors.classifier; 25 import workspaced.visitors.methodfinder; 26 27 @component("dcdext") 28 class DCDExtComponent : ComponentWrapper 29 { 30 mixin DefaultComponentWrapper; 31 32 static immutable CodeRegionProtection[] mixableProtection = [ 33 CodeRegionProtection.public_ | CodeRegionProtection.default_, 34 CodeRegionProtection.package_, CodeRegionProtection.packageIdentifier, 35 CodeRegionProtection.protected_, CodeRegionProtection.private_ 36 ]; 37 38 /// Loads dcd extension methods. Call with `{"cmd": "load", "components": ["dcdext"]}` 39 void load() 40 { 41 if (!refInstance) 42 return; 43 44 config.stringBehavior = StringBehavior.source; 45 } 46 47 /// Extracts calltips help information at a given position. 48 /// The position must be within the arguments of the function and not 49 /// outside the parentheses or inside some child call. 50 /// When generating the call parameters for a function definition, the position must be inside the normal parameters, 51 /// otherwise the template arguments will be put as normal arguments. 52 /// Returns: the position of significant locations for parameter extraction. 53 /// Params: 54 /// code = code to analyze 55 /// position = byte offset where to check for function arguments 56 /// definition = true if this hints is a function definition (templates don't have an exclamation point '!') 57 CalltipsSupport extractCallParameters(scope const(char)[] code, int position, bool definition = false) 58 { 59 auto tokens = getTokensForParser(cast(ubyte[]) code, config, &workspaced.stringCache); 60 auto queuedToken = tokens.countUntil!(a => a.index >= position) - 1; 61 if (queuedToken == -2) 62 queuedToken = cast(ptrdiff_t) tokens.length - 1; 63 else if (queuedToken == -1) 64 return CalltipsSupport.init; 65 66 bool inTemplate; 67 int depth, subDepth; 68 // contains opening parentheses location for arguments or exclamation point for templates. 69 auto startParen = queuedToken; 70 while (startParen >= 0) 71 { 72 const c = tokens[startParen]; 73 const p = startParen > 0 ? tokens[startParen - 1] : Token.init; 74 75 if (c.type == tok!"{") 76 { 77 if (subDepth == 0) 78 { 79 // we went too far, probably not inside a function (or we are in a delegate, where we don't want calltips) 80 return CalltipsSupport.init; 81 } 82 else 83 subDepth--; 84 } 85 else if (c.type == tok!"}") 86 { 87 subDepth++; 88 } 89 else if (depth == 0 && !definition && c.type == tok!"!" && p.type == tok!"identifier") 90 { 91 inTemplate = true; 92 break; 93 } 94 else if (c.type == tok!")") 95 { 96 depth++; 97 } 98 else if (c.type == tok!"(") 99 { 100 if (depth == 0 && subDepth == 0) 101 { 102 if (startParen > 1 && p.type == tok!"!" && tokens[startParen - 2].type 103 == tok!"identifier") 104 { 105 startParen--; 106 inTemplate = true; 107 } 108 break; 109 } 110 else 111 depth--; 112 } 113 startParen--; 114 } 115 116 if (startParen <= 0) 117 return CalltipsSupport.init; 118 119 auto templateOpen = inTemplate ? startParen : 0; 120 auto functionOpen = inTemplate ? 0 : startParen; 121 122 if (inTemplate) 123 { 124 // go forwards to function arguments 125 if (templateOpen + 2 < tokens.length && tokens[templateOpen + 1].type != tok!"(") 126 { 127 // single template arg (can only be one token) 128 // https://dlang.org/spec/grammar.html#TemplateSingleArgument 129 if (tokens[templateOpen + 2] == tok!"(") 130 functionOpen = templateOpen + 2; 131 } 132 else 133 { 134 functionOpen = findClosingParenForward(tokens, templateOpen + 2); 135 136 if (functionOpen >= tokens.length) 137 functionOpen = 0; 138 } 139 } 140 else 141 { 142 // go backwards to template arguments 143 if (functionOpen > 0 && tokens[functionOpen - 1].type == tok!")") 144 { 145 // multi template args 146 depth = 0; 147 subDepth = 0; 148 templateOpen = functionOpen - 1; 149 while (templateOpen >= 1) 150 { 151 const c = tokens[templateOpen]; 152 153 if (c == tok!")") 154 depth++; 155 else 156 { 157 if (depth == 0 && templateOpen > 2 && c == tok!"(" && (definition 158 || (tokens[templateOpen - 1].type == tok!"!" 159 && tokens[templateOpen - 2].type == tok!"identifier"))) 160 break; 161 else if (depth == 0) 162 { 163 templateOpen = 0; 164 break; 165 } 166 167 if (c == tok!"(") 168 depth--; 169 } 170 171 templateOpen--; 172 } 173 174 if (templateOpen <= 1) 175 templateOpen = 0; 176 } 177 else 178 { 179 // single template arg (can only be one token) 180 if (functionOpen <= 2) 181 return CalltipsSupport.init; 182 183 if (tokens[functionOpen - 2] == tok!"!" && tokens[functionOpen - 3] == tok!"identifier") 184 { 185 templateOpen = functionOpen - 2; 186 } 187 } 188 } 189 190 bool hasTemplateParens = templateOpen && templateOpen == functionOpen - 2; 191 192 depth = 0; 193 subDepth = 0; 194 bool inFuncName = true; 195 auto callStart = (templateOpen ? templateOpen : functionOpen) - 1; 196 auto funcNameStart = callStart; 197 while (callStart >= 0) 198 { 199 const c = tokens[callStart]; 200 const p = callStart > 0 ? tokens[callStart - 1] : Token.init; 201 202 if (c.type == tok!"]") 203 depth++; 204 else if (c.type == tok!"[") 205 { 206 if (depth == 0) 207 { 208 // this is some sort of `foo[(4` situation 209 return CalltipsSupport.init; 210 } 211 depth--; 212 } 213 else if (c.type == tok!")") 214 subDepth++; 215 else if (c.type == tok!"(") 216 { 217 if (subDepth == 0) 218 { 219 // this is some sort of `foo((4` situation 220 return CalltipsSupport.init; 221 } 222 subDepth--; 223 } 224 else if (depth == 0) 225 { 226 227 if (c.type.isCalltipable) 228 { 229 if (c.type == tok!"identifier" && p.type == tok!"." && (callStart < 2 230 || !tokens[callStart - 2].type.among!(tok!";", tok!",", 231 tok!"{", tok!"}", tok!"("))) 232 { 233 // member function, traverse further... 234 if (inFuncName) 235 { 236 funcNameStart = callStart; 237 inFuncName = false; 238 } 239 callStart--; 240 } 241 else 242 { 243 break; 244 } 245 } 246 else 247 { 248 // this is some sort of `4(5` or `if(4` situtation 249 return CalltipsSupport.init; 250 } 251 } 252 // we ignore stuff inside brackets and parens such as `foo[4](5).bar[6](a` 253 callStart--; 254 } 255 256 if (inFuncName) 257 funcNameStart = callStart; 258 259 auto templateClose = templateOpen ? (hasTemplateParens ? (functionOpen 260 ? functionOpen - 1 : findClosingParenForward(tokens, templateOpen + 1)) : templateOpen + 2) 261 : 0; 262 auto functionClose = functionOpen ? findClosingParenForward(tokens, functionOpen) : 0; 263 264 CalltipsSupport.Argument[] templateArgs; 265 if (templateOpen) 266 templateArgs = splitArgs(tokens[templateOpen + 1 .. templateClose]); 267 268 CalltipsSupport.Argument[] functionArgs; 269 if (functionOpen) 270 functionArgs = splitArgs(tokens[functionOpen + 1 .. functionClose]); 271 272 return CalltipsSupport([ 273 tokens.tokenIndex(templateOpen), tokens.tokenIndex(templateClose) 274 ], hasTemplateParens, templateArgs, [ 275 tokens.tokenIndex(functionOpen), tokens.tokenIndex(functionClose) 276 ], functionArgs, funcNameStart != callStart, 277 tokens.tokenIndex(funcNameStart), tokens.tokenIndex(callStart)); 278 } 279 280 /// Finds the immediate surrounding code block at a position or returns CodeBlockInfo.init for none/module block. 281 /// See_Also: CodeBlockInfo 282 CodeBlockInfo getCodeBlockRange(scope const(char)[] code, int position) 283 { 284 auto tokens = getTokensForParser(cast(ubyte[]) code, config, &workspaced.stringCache); 285 auto parsed = parseModule(tokens, "getCodeBlockRange_input.d", &rba); 286 auto reader = new CodeBlockInfoFinder(position); 287 reader.visit(parsed); 288 return reader.block; 289 } 290 291 /// Inserts a generic method after the corresponding block inside the scope where position is. 292 /// If it can't find a good spot it will insert the code properly indented ata fitting location. 293 // make public once usable 294 private CodeReplacement[] insertCodeInContainer(string insert, scope const(char)[] code, 295 int position, bool insertInLastBlock = true, bool insertAtEnd = true) 296 { 297 auto container = getCodeBlockRange(code, position); 298 299 scope const(char)[] codeBlock = code[container.innerRange[0] .. container.innerRange[1]]; 300 301 scope tokensInsert = getTokensForParser(cast(ubyte[]) insert, config, 302 &workspaced.stringCache); 303 scope parsedInsert = parseModule(tokensInsert, "insertCode_insert.d", &rba); 304 305 scope insertReader = new CodeDefinitionClassifier(insert); 306 insertReader.visit(parsedInsert); 307 scope insertRegions = insertReader.regions.sort!"a.type < b.type".uniq.array; 308 309 scope tokens = getTokensForParser(cast(ubyte[]) codeBlock, config, &workspaced.stringCache); 310 scope parsed = parseModule(tokens, "insertCode_code.d", &rba); 311 312 scope reader = new CodeDefinitionClassifier(codeBlock); 313 reader.visit(parsed); 314 scope regions = reader.regions; 315 316 CodeReplacement[] ret; 317 318 foreach (CodeDefinitionClassifier.Region toInsert; insertRegions) 319 { 320 auto insertCode = insert[toInsert.region[0] .. toInsert.region[1]]; 321 scope existing = regions.enumerate.filter!(a => a.value.sameBlockAs(toInsert)); 322 if (existing.empty) 323 { 324 const checkProtection = CodeRegionProtection.init.reduce!"a | b"( 325 mixableProtection.filter!(a => (a & toInsert.protection) != 0)); 326 327 bool inIncompatible = false; 328 bool lastFit = false; 329 int fittingProtection = -1; 330 int firstStickyProtection = -1; 331 int regionAfterFitting = -1; 332 foreach (i, stickyProtection; regions) 333 { 334 if (stickyProtection.affectsFollowing 335 && stickyProtection.protection != CodeRegionProtection.init) 336 { 337 if (firstStickyProtection == -1) 338 firstStickyProtection = cast(int) i; 339 340 if ((stickyProtection.protection & checkProtection) != 0) 341 { 342 fittingProtection = cast(int) i; 343 lastFit = true; 344 if (!insertInLastBlock) 345 break; 346 } 347 else 348 { 349 if (lastFit) 350 { 351 regionAfterFitting = cast(int) i; 352 lastFit = false; 353 } 354 inIncompatible = true; 355 } 356 } 357 } 358 assert(firstStickyProtection != -1 || !inIncompatible); 359 assert(regionAfterFitting != -1 || fittingProtection == -1 || !inIncompatible); 360 361 if (inIncompatible) 362 { 363 int insertRegion = fittingProtection == -1 ? firstStickyProtection : regionAfterFitting; 364 insertCode = text(indent(insertCode, regions[insertRegion].minIndentation), "\n\n"); 365 auto len = cast(uint) insertCode.length; 366 367 toInsert.region[0] = regions[insertRegion].region[0]; 368 toInsert.region[1] = regions[insertRegion].region[0] + len; 369 foreach (ref r; regions[insertRegion .. $]) 370 { 371 r.region[0] += len; 372 r.region[1] += len; 373 } 374 } 375 else 376 { 377 auto lastRegion = regions.back; 378 insertCode = indent(insertCode, lastRegion.minIndentation).idup; 379 auto len = cast(uint) insertCode.length; 380 toInsert.region[0] = lastRegion.region[1]; 381 toInsert.region[1] = lastRegion.region[1] + len; 382 } 383 regions ~= toInsert; 384 ret ~= CodeReplacement([toInsert.region[0], toInsert.region[0]], insertCode); 385 } 386 else 387 { 388 auto target = insertInLastBlock ? existing.tail(1).front : existing.front; 389 390 insertCode = text("\n\n", indent(insertCode, regions[target.index].minIndentation)); 391 const codeLength = cast(int) insertCode.length; 392 393 if (insertAtEnd) 394 { 395 ret ~= CodeReplacement([ 396 target.value.region[1], target.value.region[1] 397 ], insertCode); 398 toInsert.region[0] = target.value.region[1]; 399 toInsert.region[1] = target.value.region[1] + codeLength; 400 regions[target.index].region[1] = toInsert.region[1]; 401 foreach (ref other; regions[target.index + 1 .. $]) 402 { 403 other.region[0] += codeLength; 404 other.region[1] += codeLength; 405 } 406 } 407 else 408 { 409 ret ~= CodeReplacement([ 410 target.value.region[0], target.value.region[0] 411 ], insertCode); 412 regions[target.index].region[1] += codeLength; 413 foreach (ref other; regions[target.index + 1 .. $]) 414 { 415 other.region[0] += codeLength; 416 other.region[1] += codeLength; 417 } 418 } 419 } 420 } 421 422 return ret; 423 } 424 425 /// Implements the interfaces or abstract classes of a specified class/interface. 426 Future!string implement(scope const(char)[] code, int position) 427 { 428 auto ret = new Future!string; 429 gthreads.create({ 430 mixin(traceTask); 431 try 432 { 433 struct InterfaceTree 434 { 435 InterfaceDetails details; 436 InterfaceTree[] inherits; 437 } 438 439 auto baseInterface = getInterfaceDetails("stdin", code, position); 440 441 string[] implementedMethods = baseInterface.methods 442 .filter!"!a.needsImplementation" 443 .map!"a.identifier" 444 .array; 445 446 // start with private, add all the public ones later in traverseTree 447 FieldDetails[] availableVariables = baseInterface.fields.filter!"a.isPrivate".array; 448 InterfaceTree tree = InterfaceTree(baseInterface); 449 450 InterfaceTree* treeByName(InterfaceTree* tree, string name) 451 { 452 if (tree.details.name == name) 453 return tree; 454 foreach (ref parent; tree.inherits) 455 { 456 InterfaceTree* t = treeByName(&parent, name); 457 if (t !is null) 458 return t; 459 } 460 return null; 461 } 462 463 void traverseTree(ref InterfaceTree sub) 464 { 465 availableVariables ~= sub.details.fields.filter!"!a.isPrivate".array; 466 foreach (i, parent; sub.details.parentPositions) 467 { 468 string parentName = sub.details.normalizedParents[i]; 469 if (treeByName(&tree, parentName) is null) 470 { 471 auto details = lookupInterface(sub.details.code, parent); 472 sub.inherits ~= InterfaceTree(details); 473 } 474 } 475 foreach (ref inherit; sub.inherits) 476 traverseTree(inherit); 477 } 478 479 traverseTree(tree); 480 481 string changes; 482 void processTree(ref InterfaceTree tree) 483 { 484 auto details = tree.details; 485 if (details.methods.length) 486 { 487 bool first = true; 488 foreach (fn; details.methods) 489 { 490 if (implementedMethods.canFind(fn.identifier)) 491 continue; 492 if (!fn.needsImplementation) 493 { 494 implementedMethods ~= fn.identifier; 495 continue; 496 } 497 if (first) 498 { 499 changes ~= "// implement " ~ details.name ~ "\n\n"; 500 first = false; 501 } 502 if (details.needsOverride) 503 changes ~= "override "; 504 changes ~= fn.signature[0 .. $ - 1]; 505 changes ~= " {"; 506 if (fn.optionalImplementation) 507 { 508 changes ~= "\n\t// TODO: optional implementation\n"; 509 } 510 511 string propertySearch; 512 if (fn.signature.canFind("@property") && fn.arguments.length <= 1) 513 propertySearch = fn.name; 514 else if ((fn.name.startsWith("get") && fn.arguments.length == 0) 515 || (fn.name.startsWith("set") && fn.arguments.length == 1)) 516 propertySearch = fn.name[3 .. $]; 517 518 string foundProperty; 519 if (propertySearch) 520 { 521 foreach (variable; availableVariables) 522 { 523 if (fieldNameMatches(variable.name, propertySearch)) 524 { 525 foundProperty = variable.name; 526 break; 527 } 528 } 529 } 530 531 if (foundProperty.length) 532 { 533 changes ~= "\n\t"; 534 if (fn.returnType != "void") 535 changes ~= "return "; 536 if (fn.name.startsWith("set") || fn.arguments.length == 1) 537 changes ~= foundProperty ~ " = " ~ fn.arguments[0].name; 538 else 539 changes ~= foundProperty; 540 changes ~= ";\n"; 541 } 542 else if (fn.hasBody) 543 { 544 changes ~= "\n\t"; 545 if (fn.returnType != "void") 546 changes ~= "return "; 547 changes ~= "super." ~ fn.name; 548 if (fn.arguments.length) 549 changes ~= "(" ~ format("%(%s, %)", fn.arguments) ~ ")"; 550 else if (fn.returnType == "void") 551 changes ~= "()"; // make functions that don't return add (), otherwise they might be attributes and don't need that 552 changes ~= ";\n"; 553 } 554 else if (fn.returnType != "void") 555 { 556 changes ~= "\n\t"; 557 if (fn.isNothrowOrNogc) 558 { 559 if (fn.returnType.endsWith("[]")) 560 changes ~= "return null; // TODO: implement"; 561 else 562 changes ~= "return " ~ fn.returnType ~ ".init; // TODO: implement"; 563 } 564 else 565 changes ~= `assert(false, "Method ` ~ fn.name ~ ` not implemented");`; 566 changes ~= "\n"; 567 } 568 changes ~= "}\n\n"; 569 } 570 } 571 572 foreach (parent; tree.inherits) 573 processTree(parent); 574 } 575 576 processTree(tree); 577 578 ret.finish(changes); 579 } 580 catch (Throwable t) 581 { 582 ret.error(t); 583 } 584 }); 585 return ret; 586 } 587 588 private: 589 RollbackAllocator rba; 590 LexerConfig config; 591 592 InterfaceDetails lookupInterface(scope const(char)[] code, int position) 593 { 594 auto data = get!DCDComponent.findDeclaration(code, position).getBlocking; 595 string file = data.file; 596 int newPosition = data.position; 597 598 if (!file.length) 599 return InterfaceDetails.init; 600 601 auto newCode = code; 602 if (file != "stdin") 603 newCode = readText(file); 604 605 return getInterfaceDetails(file, newCode, newPosition); 606 } 607 608 InterfaceDetails getInterfaceDetails(string file, scope const(char)[] code, int position) 609 { 610 auto tokens = getTokensForParser(cast(ubyte[]) code, config, &workspaced.stringCache); 611 auto parsed = parseModule(tokens, file, &rba); 612 auto reader = new InterfaceMethodFinder(code, position); 613 reader.visit(parsed); 614 return reader.details; 615 } 616 } 617 618 /// 619 enum CodeRegionType : int 620 { 621 /// null region (unset) 622 init, 623 /// Imports inside the block 624 imports = 1 << 0, 625 /// Aliases `alias foo this;`, `alias Type = Other;` 626 aliases = 1 << 1, 627 /// Nested classes/structs/unions/etc. 628 types = 1 << 2, 629 /// Raw variables `Type name;` 630 fields = 1 << 3, 631 /// Normal constructors `this(Args args)` 632 ctor = 1 << 4, 633 /// Copy constructors `this(this)` 634 copyctor = 1 << 5, 635 /// Destructors `~this()` 636 dtor = 1 << 6, 637 /// Properties (functions annotated with `@property`) 638 properties = 1 << 7, 639 /// Regular functions 640 methods = 1 << 8, 641 } 642 643 /// 644 enum CodeRegionProtection : int 645 { 646 /// null protection (unset) 647 init, 648 /// default (unmarked) protection 649 default_ = 1 << 0, 650 /// public protection 651 public_ = 1 << 1, 652 /// package (automatic) protection 653 package_ = 1 << 2, 654 /// package (manual package name) protection 655 packageIdentifier = 1 << 3, 656 /// protected protection 657 protected_ = 1 << 4, 658 /// private protection 659 private_ = 1 << 5, 660 } 661 662 /// 663 enum CodeRegionStatic : int 664 { 665 /// null static (unset) 666 init, 667 /// non-static code 668 instanced = 1 << 0, 669 /// static code 670 static_ = 1 << 1, 671 } 672 673 /// Represents a class/interface/struct/union/template with body. 674 struct CodeBlockInfo 675 { 676 /// 677 enum Type : int 678 { 679 // keep the underlines in these values for range checking properly 680 681 /// 682 class_, 683 /// 684 interface_, 685 /// 686 struct_, 687 /// 688 union_, 689 /// 690 template_, 691 } 692 693 static immutable string[] typePrefixes = [ 694 "class ", "interface ", "struct ", "union ", "template " 695 ]; 696 697 /// 698 Type type; 699 /// 700 string name; 701 /// Outer range inside the code spanning curly braces and name but not type keyword. 702 uint[2] outerRange; 703 /// Inner range of body of the block touching, but not spanning curly braces. 704 uint[2] innerRange; 705 706 string prefix() @property 707 { 708 return typePrefixes[cast(int) type]; 709 } 710 } 711 712 /// 713 struct CalltipsSupport 714 { 715 /// 716 struct Argument 717 { 718 /// Ranges of type, name and value not including commas or parentheses, but being right next to them. For calls this is the only important and accurate value. 719 int[2] contentRange; 720 /// Range of just the type, or for templates also `alias` 721 int[2] typeRange; 722 /// Range of just the name 723 int[2] nameRange; 724 /// Range of just the default value 725 int[2] valueRange; 726 727 /// Creates Argument(range, range, range, 0) 728 static Argument templateType(int[2] range) 729 { 730 return Argument(range, range, range); 731 } 732 733 /// Creates Argument(range, 0, range, range) 734 static Argument templateValue(int[2] range) 735 { 736 return Argument(range, typeof(range).init, range, range); 737 } 738 } 739 740 bool hasTemplate() @property 741 { 742 return hasTemplateParens || templateArgumentsRange != typeof(templateArgumentsRange).init; 743 } 744 745 /// Range starting inclusive at exclamation point until exclusive at closing bracket or function opening bracket. 746 int[2] templateArgumentsRange; 747 /// 748 bool hasTemplateParens; 749 /// 750 Argument[] templateArgs; 751 /// Range starting inclusive at opening parentheses until exclusive at closing parentheses. 752 int[2] functionParensRange; 753 /// 754 Argument[] functionArgs; 755 /// True if the function is UFCS or a member function of some object or namespace. 756 /// False if this is a global function call. 757 bool hasParent; 758 /// Start of the function itself. 759 int functionStart; 760 /// Start of the whole call going up all call parents. (`foo.bar.function` having `foo.bar` as parents) 761 int parentStart; 762 } 763 764 private: 765 766 bool isCalltipable(IdType type) 767 { 768 return type == tok!"identifier" || type == tok!"assert" || type == tok!"import" 769 || type == tok!"mixin" || type == tok!"super" || type == tok!"this" || type == tok!"__traits"; 770 } 771 772 int[2] tokenRange(const Token token) 773 { 774 return [cast(int) token.index, cast(int)(token.index + token.text.length)]; 775 } 776 777 int tokenEnd(const Token token) 778 { 779 return cast(int)(token.index + token.text.length); 780 } 781 782 int tokenIndex(const(Token)[] tokens, ptrdiff_t i) 783 { 784 if (i > 0 && i == tokens.length) 785 return cast(int)(tokens[$ - 1].index + tokens[$ - 1].text.length); 786 return i >= 0 ? cast(int) tokens[i].index : 0; 787 } 788 789 int tokenEndIndex(const(Token)[] tokens, ptrdiff_t i) 790 { 791 if (i > 0 && i == tokens.length) 792 return cast(int)(tokens[$ - 1].index + tokens[$ - 1].text.length); 793 return i >= 0 ? cast(int)(tokens[i].index + tokens[i].text.length) : 0; 794 } 795 796 ptrdiff_t findClosingParenForward(const(Token)[] tokens, ptrdiff_t open) 797 in(tokens[open].type == tok!"(") 798 { 799 if (open >= tokens.length || open < 0) 800 return open; 801 802 open++; 803 804 int depth = 1; 805 int subDepth = 0; 806 while (open < tokens.length) 807 { 808 const c = tokens[open]; 809 810 if (c == tok!"(") 811 depth++; 812 else if (c == tok!"{") 813 subDepth++; 814 else if (c == tok!"}") 815 { 816 if (subDepth == 0) 817 break; 818 subDepth--; 819 } 820 else 821 { 822 if (c == tok!";" && subDepth == 0) 823 break; 824 825 if (c == tok!")") 826 depth--; 827 828 if (depth == 0) 829 break; 830 } 831 832 open++; 833 } 834 return open; 835 } 836 837 CalltipsSupport.Argument[] splitArgs(const(Token)[] tokens) 838 { 839 auto ret = appender!(CalltipsSupport.Argument[]); 840 size_t start = 0; 841 size_t valueStart = 0; 842 843 int depth, subDepth; 844 bool gotValue; 845 846 void putArg(size_t end) 847 { 848 if (start >= end || start >= tokens.length) 849 return; 850 851 CalltipsSupport.Argument arg; 852 853 auto typename = tokens[start .. end]; 854 arg.contentRange = [cast(int) typename[0].index, typename[$ - 1].tokenEnd]; 855 if (typename.length == 1) 856 { 857 auto t = typename[0]; 858 if (t.type == tok!"identifier" || t.type.isBasicType) 859 arg = CalltipsSupport.Argument.templateType(t.tokenRange); 860 else 861 arg = CalltipsSupport.Argument.templateValue(t.tokenRange); 862 } 863 else 864 { 865 if (gotValue && valueStart > start && valueStart <= end) 866 { 867 typename = tokens[start .. valueStart]; 868 auto val = tokens[valueStart .. end]; 869 if (val.length) 870 arg.valueRange = [cast(int) val[0].index, val[$ - 1].tokenEnd]; 871 } 872 873 else if (typename.length == 1) 874 { 875 auto t = typename[0]; 876 if (t.type == tok!"identifier" || t.type.isBasicType) 877 arg.typeRange = arg.nameRange = t.tokenRange; 878 else 879 arg.typeRange = t.tokenRange; 880 } 881 else if (typename.length) 882 { 883 if (typename[$ - 1].type == tok!"identifier") 884 { 885 arg.nameRange = typename[$ - 1].tokenRange; 886 typename = typename[0 .. $ - 1]; 887 } 888 arg.typeRange = [cast(int) typename[0].index, typename[$ - 1].tokenEnd]; 889 } 890 } 891 892 ret.put(arg); 893 894 gotValue = false; 895 start = end + 1; 896 } 897 898 foreach (i, token; tokens) 899 { 900 if (token.type == tok!"{") 901 subDepth++; 902 else if (token.type == tok!"}") 903 { 904 if (subDepth == 0) 905 break; 906 subDepth--; 907 } 908 else if (token.type == tok!"(" || token.type == tok!"[") 909 depth++; 910 else if (token.type == tok!")" || token.type == tok!"]") 911 { 912 if (depth == 0) 913 break; 914 depth--; 915 } 916 917 if (token.type == tok!",") 918 putArg(i); 919 else if (token.type == tok!":" || token.type == tok!"=") 920 { 921 if (!gotValue) 922 { 923 valueStart = i + 1; 924 gotValue = true; 925 } 926 } 927 } 928 putArg(tokens.length); 929 930 return ret.data; 931 } 932 933 auto indent(scope const(char)[] code, string indentation) 934 { 935 return code.lineSplitter!(KeepTerminator.yes) 936 .map!(a => a.length ? indentation ~ a : a) 937 .join; 938 } 939 940 bool fieldNameMatches(string field, in char[] expected) 941 { 942 import std.uni : sicmp; 943 944 if (field.startsWith("_")) 945 field = field[1 .. $]; 946 else if (field.startsWith("m_")) 947 field = field[2 .. $]; 948 else if (field.length >= 2 && field[0] == 'm' && field[1].isUpper) 949 field = field[1 .. $]; 950 951 return field.sicmp(expected) == 0; 952 } 953 954 final class CodeBlockInfoFinder : ASTVisitor 955 { 956 this(int targetPosition) 957 { 958 this.targetPosition = targetPosition; 959 } 960 961 override void visit(const ClassDeclaration dec) 962 { 963 visitContainer(dec.name, CodeBlockInfo.Type.class_, dec.structBody); 964 } 965 966 override void visit(const InterfaceDeclaration dec) 967 { 968 visitContainer(dec.name, CodeBlockInfo.Type.interface_, dec.structBody); 969 } 970 971 override void visit(const StructDeclaration dec) 972 { 973 visitContainer(dec.name, CodeBlockInfo.Type.struct_, dec.structBody); 974 } 975 976 override void visit(const UnionDeclaration dec) 977 { 978 visitContainer(dec.name, CodeBlockInfo.Type.union_, dec.structBody); 979 } 980 981 override void visit(const TemplateDeclaration dec) 982 { 983 if (cast(int) targetPosition >= cast(int) dec.name.index && targetPosition < dec.endLocation) 984 { 985 block = CodeBlockInfo.init; 986 block.type = CodeBlockInfo.Type.template_; 987 block.name = dec.name.text; 988 block.outerRange = [ 989 cast(uint) dec.name.index, cast(uint) dec.endLocation + 1 990 ]; 991 block.innerRange = [ 992 cast(uint) dec.startLocation + 1, cast(uint) dec.endLocation 993 ]; 994 dec.accept(this); 995 } 996 } 997 998 private void visitContainer(const Token name, CodeBlockInfo.Type type, const StructBody structBody) 999 { 1000 if (!structBody) 1001 return; 1002 if (cast(int) targetPosition >= cast(int) name.index && targetPosition < structBody.endLocation) 1003 { 1004 block = CodeBlockInfo.init; 1005 block.type = type; 1006 block.name = name.text; 1007 block.outerRange = [ 1008 cast(uint) name.index, cast(uint) structBody.endLocation + 1 1009 ]; 1010 block.innerRange = [ 1011 cast(uint) structBody.startLocation + 1, cast(uint) structBody.endLocation 1012 ]; 1013 structBody.accept(this); 1014 } 1015 } 1016 1017 alias visit = ASTVisitor.visit; 1018 1019 CodeBlockInfo block; 1020 int targetPosition; 1021 } 1022 1023 version (unittest) static immutable string SimpleClassTestCode = q{ 1024 module foo; 1025 1026 class FooBar 1027 { 1028 public: 1029 int i; // default instanced fields 1030 string s; 1031 long l; 1032 1033 public this() // public instanced ctor 1034 { 1035 i = 4; 1036 } 1037 1038 protected: 1039 int x; // protected instanced field 1040 1041 private: 1042 static const int foo() @nogc nothrow pure @system // private static methods 1043 { 1044 if (s == "a") 1045 { 1046 i = 5; 1047 } 1048 } 1049 1050 static void bar1() {} 1051 1052 void bar2() {} // private instanced methods 1053 void bar3() {} 1054 }}; 1055 1056 unittest 1057 { 1058 scope backend = new WorkspaceD(); 1059 auto workspace = makeTemporaryTestingWorkspace; 1060 auto instance = backend.addInstance(workspace.directory); 1061 backend.register!DCDExtComponent; 1062 DCDExtComponent dcdext = instance.get!DCDExtComponent; 1063 1064 assert(dcdext.getCodeBlockRange(SimpleClassTestCode, 123) == CodeBlockInfo(CodeBlockInfo.Type.class_, 1065 "FooBar", [20, SimpleClassTestCode.length], [ 1066 28, SimpleClassTestCode.length - 1 1067 ])); 1068 assert(dcdext.getCodeBlockRange(SimpleClassTestCode, 19) == CodeBlockInfo.init); 1069 assert(dcdext.getCodeBlockRange(SimpleClassTestCode, 20) != CodeBlockInfo.init); 1070 1071 auto replacements = dcdext.insertCodeInContainer("void foo()\n{\n\twriteln();\n}", 1072 SimpleClassTestCode, 123); 1073 import std.stdio; 1074 1075 stderr.writeln(replacements); 1076 } 1077 1078 unittest 1079 { 1080 import std.conv; 1081 1082 scope backend = new WorkspaceD(); 1083 auto workspace = makeTemporaryTestingWorkspace; 1084 auto instance = backend.addInstance(workspace.directory); 1085 backend.register!DCDExtComponent; 1086 DCDExtComponent dcdext = instance.get!DCDExtComponent; 1087 1088 import std.stdio; 1089 1090 auto extract = dcdext.extractCallParameters("int x; foo.bar(4, fgerg\n\nfoo(); int y;", 23); 1091 assert(!extract.hasTemplate); 1092 assert(extract.parentStart == 7); 1093 assert(extract.functionStart == 11); 1094 assert(extract.functionParensRange[0] == 14); 1095 assert(extract.functionParensRange[1] <= 31); 1096 assert(extract.functionArgs.length == 2); 1097 assert(extract.functionArgs[0].contentRange == [15, 16]); 1098 assert(extract.functionArgs[1].contentRange[0] == 18); 1099 assert(extract.functionArgs[1].contentRange[1] <= 31); 1100 1101 extract = dcdext.extractCallParameters("int x; foo.bar(4, fgerg)\n\nfoo(); int y;", 23); 1102 assert(!extract.hasTemplate); 1103 assert(extract.parentStart == 7); 1104 assert(extract.functionStart == 11); 1105 assert(extract.functionParensRange == [14, 23]); 1106 assert(extract.functionArgs.length == 2); 1107 assert(extract.functionArgs[0].contentRange == [15, 16]); 1108 assert(extract.functionArgs[1].contentRange == [18, 23]); 1109 }