1 /** 2 * This file is part of DCD, a development tool for the D programming language. 3 * Copyright (C) 2014 Brian Schott 4 * 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation, either version 3 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 */ 18 19 module server.autocomplete; 20 21 import std.algorithm; 22 import std.experimental.allocator; 23 import std.experimental.logger; 24 import std.array; 25 import std.conv; 26 import std.experimental.logger; 27 import std.file; 28 import std.path; 29 import std.range; 30 import std.stdio; 31 import std.string; 32 import std.typecons; 33 import std.uni; 34 35 import dparse.ast; 36 import dparse.lexer; 37 import dparse.parser; 38 import dparse.rollback_allocator; 39 40 import dsymbol.conversion; 41 import dsymbol.modulecache; 42 import dsymbol.string_interning; 43 import dsymbol.symbol; 44 import dsymbol.scope_; 45 import dsymbol.builtin.names; 46 import dsymbol.builtin.symbols; 47 48 import common.constants; 49 import common.messages; 50 51 import containers.hashset; 52 53 /** 54 * Finds the uses of the symbol at the cursor position within a single document. 55 * Params: 56 * request = the autocompletion request. 57 * Returns: 58 * the autocompletion response. 59 */ 60 public AutocompleteResponse findLocalUse(AutocompleteRequest request, 61 ref ModuleCache moduleCache) 62 { 63 AutocompleteResponse response; 64 RollbackAllocator rba; 65 auto allocator = scoped!(ASTAllocator)(); 66 auto cache = StringCache(StringCache.defaultBucketCount); 67 68 // patchs the original request for the subsequent requests 69 request.kind = RequestKind.symbolLocation; 70 71 // getSymbolsForCompletion() copy to avoid repetitive parsing 72 LexerConfig config; 73 config.fileName = ""; 74 const(Token)[] tokenArray = getTokensForParser(cast(ubyte[]) request.sourceCode, 75 config, &cache); 76 SymbolStuff getSymbolsAtCursor(size_t cursorPosition) 77 { 78 auto sortedTokens = assumeSorted(tokenArray); 79 auto beforeTokens = sortedTokens.lowerBound(cursorPosition); 80 ScopeSymbolPair pair = generateAutocompleteTrees(tokenArray, allocator, 81 &rba, request.cursorPosition, moduleCache); 82 auto expression = getExpression(beforeTokens); 83 return SymbolStuff(getSymbolsByTokenChain(pair.scope_, expression, 84 cursorPosition, CompletionType.location), pair.symbol, pair.scope_); 85 } 86 87 // gets the symbol matching to cursor pos 88 SymbolStuff stuff = getSymbolsAtCursor(cast(size_t)request.cursorPosition); 89 scope(exit) stuff.destroy(); 90 91 // starts searching only if no ambiguity with the symbol 92 if (stuff.symbols.length == 1) 93 { 94 const(DSymbol*) sourceSymbol = stuff.symbols[0]; 95 response.symbolLocation = sourceSymbol.location; 96 response.symbolFilePath = sourceSymbol.symbolFile.idup; 97 98 // gets the source token to avoid too much getSymbolsAtCursor() 99 const(Token)* sourceToken; 100 foreach(i, t; tokenArray) 101 { 102 if (t.type != tok!"identifier") 103 continue; 104 if (request.cursorPosition >= t.index && 105 request.cursorPosition < t.index + t.text.length) 106 { 107 sourceToken = tokenArray.ptr + i; 108 break; 109 } 110 } 111 112 // finds the tokens that match to the source symbol 113 if (sourceToken != null) foreach (t; tokenArray) 114 { 115 if (t.type == tok!"identifier" && t.text == sourceToken.text) 116 { 117 size_t pos = cast(size_t) t.index + 1; // place cursor inside the token 118 SymbolStuff candidate = getSymbolsAtCursor(pos); 119 scope(exit) candidate.destroy(); 120 if (candidate.symbols.length == 1 && 121 candidate.symbols[0].location == sourceSymbol.location && 122 candidate.symbols[0].symbolFile == sourceSymbol.symbolFile) 123 { 124 response.locations ~= t.index; 125 } 126 } 127 } 128 else 129 { 130 warning("The source token is not an identifier"); 131 } 132 } 133 else 134 { 135 warning("No or ambiguous symbol for the identifier at cursor"); 136 } 137 return response; 138 } 139 140 /** 141 * Gets documentation for the symbol at the cursor 142 * Params: 143 * request = the autocompletion request 144 * Returns: 145 * the autocompletion response 146 */ 147 public AutocompleteResponse getDoc(const AutocompleteRequest request, 148 ref ModuleCache moduleCache) 149 { 150 // trace("Getting doc comments"); 151 AutocompleteResponse response; 152 RollbackAllocator rba; 153 auto allocator = scoped!(ASTAllocator)(); 154 auto cache = StringCache(StringCache.defaultBucketCount); 155 SymbolStuff stuff = getSymbolsForCompletion(request, CompletionType.ddoc, 156 allocator, &rba, cache, moduleCache); 157 if (stuff.symbols.length == 0) 158 warning("Could not find symbol"); 159 else 160 { 161 Appender!(char[]) app; 162 163 bool isDitto(string s) 164 { 165 import std.uni : icmp; 166 if (s.length > 5) 167 return false; 168 else 169 return s.icmp("ditto") == 0; 170 } 171 172 void putDDocChar(char c) 173 { 174 switch (c) 175 { 176 case '\\': 177 app.put('\\'); 178 app.put('\\'); 179 break; 180 case '\n': 181 app.put('\\'); 182 app.put('n'); 183 break; 184 default: 185 app.put(c); 186 break; 187 } 188 } 189 190 void putDDocString(string s) 191 { 192 foreach (char c; s) 193 putDDocChar(c); 194 } 195 196 foreach(ref symbol; stuff.symbols.filter!(a => !a.doc.empty && !isDitto(a.doc))) 197 { 198 app.clear; 199 putDDocString(symbol.doc); 200 response.docComments ~= app.data.idup; 201 } 202 } 203 return response; 204 } 205 206 /** 207 * Finds the declaration of the symbol at the cursor position. 208 * Params: 209 * request = the autocompletion request 210 * Returns: 211 * the autocompletion response 212 */ 213 public AutocompleteResponse findDeclaration(const AutocompleteRequest request, 214 ref ModuleCache moduleCache) 215 { 216 AutocompleteResponse response; 217 RollbackAllocator rba; 218 auto allocator = scoped!(ASTAllocator)(); 219 auto cache = StringCache(StringCache.defaultBucketCount); 220 SymbolStuff stuff = getSymbolsForCompletion(request, 221 CompletionType.location, allocator, &rba, cache, moduleCache); 222 scope(exit) stuff.destroy(); 223 if (stuff.symbols.length > 0) 224 { 225 response.symbolLocation = stuff.symbols[0].location; 226 response.symbolFilePath = stuff.symbols[0].symbolFile.idup; 227 } 228 else 229 warning("Could not find symbol declaration"); 230 return response; 231 } 232 233 /** 234 * Handles autocompletion 235 * Params: 236 * request = the autocompletion request 237 * Returns: 238 * the autocompletion response 239 */ 240 public AutocompleteResponse complete(const AutocompleteRequest request, 241 ref ModuleCache moduleCache) 242 { 243 const(Token)[] tokenArray; 244 auto stringCache = StringCache(StringCache.defaultBucketCount); 245 auto beforeTokens = getTokensBeforeCursor(request.sourceCode, 246 request.cursorPosition, stringCache, tokenArray); 247 if (beforeTokens.length >= 2) 248 { 249 if (beforeTokens[$ - 1] == tok!"(" || beforeTokens[$ - 1] == tok!"[") 250 { 251 return parenCompletion(beforeTokens, tokenArray, request.cursorPosition, 252 moduleCache); 253 } 254 else if (beforeTokens[$ - 1] == tok!",") 255 { 256 immutable size_t end = goBackToOpenParen(beforeTokens); 257 if (end != size_t.max) 258 return parenCompletion(beforeTokens[0 .. end], tokenArray, 259 request.cursorPosition, moduleCache); 260 } 261 else 262 { 263 ImportKind kind = determineImportKind(beforeTokens); 264 if (kind == ImportKind.neither) 265 { 266 if (beforeTokens.isUdaExpression) 267 beforeTokens = beforeTokens[$-1 .. $]; 268 return dotCompletion(beforeTokens, tokenArray, request.cursorPosition, 269 moduleCache); 270 } 271 else 272 return importCompletion(beforeTokens, kind, moduleCache); 273 } 274 } 275 return dotCompletion(beforeTokens, tokenArray, request.cursorPosition, moduleCache); 276 } 277 278 /** 279 * 280 */ 281 public AutocompleteResponse symbolSearch(const AutocompleteRequest request, 282 ref ModuleCache moduleCache) 283 { 284 import containers.ttree : TTree; 285 286 LexerConfig config; 287 config.fileName = ""; 288 auto cache = StringCache(StringCache.defaultBucketCount); 289 const(Token)[] tokenArray = getTokensForParser(cast(ubyte[]) request.sourceCode, 290 config, &cache); 291 auto allocator = scoped!(ASTAllocator)(); 292 RollbackAllocator rba; 293 ScopeSymbolPair pair = generateAutocompleteTrees(tokenArray, allocator, 294 &rba, request.cursorPosition, moduleCache); 295 scope(exit) pair.destroy(); 296 297 static struct SearchResults 298 { 299 void put(const(DSymbol)* symbol) 300 { 301 tree.insert(SearchResult(symbol)); 302 } 303 304 static struct SearchResult 305 { 306 const(DSymbol)* symbol; 307 308 int opCmp(ref const SearchResult other) const pure nothrow 309 { 310 if (other.symbol.symbolFile < symbol.symbolFile) 311 return -1; 312 if (other.symbol.symbolFile > symbol.symbolFile) 313 return 1; 314 if (other.symbol.location < symbol.location) 315 return -1; 316 return other.symbol.location > symbol.location; 317 } 318 } 319 320 TTree!(SearchResult) tree; 321 } 322 323 SearchResults results; 324 HashSet!size_t visited; 325 foreach (symbol; pair.scope_.symbols) 326 symbol.getParts!SearchResults(internString(request.searchName), results, visited); 327 foreach (s; moduleCache.getAllSymbols()) 328 s.symbol.getParts!SearchResults(internString(request.searchName), results, visited); 329 330 AutocompleteResponse response; 331 foreach (result; results.tree[]) 332 { 333 response.locations ~= result.symbol.location; 334 response.completionKinds ~= result.symbol.kind; 335 response.completions ~= result.symbol.symbolFile; 336 } 337 338 return response; 339 } 340 341 /******************************************************************************/ 342 private: 343 344 enum ImportKind : ubyte 345 { 346 selective, 347 normal, 348 neither 349 } 350 351 /** 352 * Handles dot completion for identifiers and types. 353 * Params: 354 * beforeTokens = the tokens before the cursor 355 * tokenArray = all tokens in the file 356 * cursorPosition = the cursor position in bytes 357 * Returns: 358 * the autocompletion response 359 */ 360 AutocompleteResponse dotCompletion(T)(T beforeTokens, const(Token)[] tokenArray, 361 size_t cursorPosition, ref ModuleCache moduleCache) 362 { 363 AutocompleteResponse response; 364 365 // Partial symbol name appearing after the dot character and before the 366 // cursor. 367 string partial; 368 369 // Type of the token before the dot, or identifier if the cursor was at 370 // an identifier. 371 IdType significantTokenType; 372 373 if (beforeTokens.length >= 1 && beforeTokens[$ - 1] == tok!"identifier") 374 { 375 // Set partial to the slice of the identifier between the beginning 376 // of the identifier and the cursor. This improves the completion 377 // responses when the cursor is in the middle of an identifier instead 378 // of at the end 379 auto t = beforeTokens[$ - 1]; 380 if (cursorPosition - t.index >= 0 && cursorPosition - t.index <= t.text.length) 381 partial = t.text[0 .. cursorPosition - t.index]; 382 significantTokenType = tok!"identifier"; 383 beforeTokens = beforeTokens[0 .. $ - 1]; 384 } 385 else if (beforeTokens.length >= 2 && beforeTokens[$ - 1] == tok!".") 386 significantTokenType = beforeTokens[$ - 2].type; 387 else 388 return response; 389 390 switch (significantTokenType) 391 { 392 case tok!"stringLiteral": 393 case tok!"wstringLiteral": 394 case tok!"dstringLiteral": 395 foreach (symbol; arraySymbols) 396 { 397 response.completionKinds ~= symbol.kind; 398 response.completions ~= symbol.name.dup; 399 } 400 response.completionType = CompletionType.identifiers; 401 break; 402 case tok!"int": 403 case tok!"uint": 404 case tok!"long": 405 case tok!"ulong": 406 case tok!"char": 407 case tok!"wchar": 408 case tok!"dchar": 409 case tok!"bool": 410 case tok!"byte": 411 case tok!"ubyte": 412 case tok!"short": 413 case tok!"ushort": 414 case tok!"cent": 415 case tok!"ucent": 416 case tok!"float": 417 case tok!"ifloat": 418 case tok!"cfloat": 419 case tok!"idouble": 420 case tok!"cdouble": 421 case tok!"double": 422 case tok!"real": 423 case tok!"ireal": 424 case tok!"creal": 425 case tok!"identifier": 426 case tok!")": 427 case tok!"]": 428 case tok!"this": 429 case tok!"super": 430 auto allocator = scoped!(ASTAllocator)(); 431 RollbackAllocator rba; 432 ScopeSymbolPair pair = generateAutocompleteTrees(tokenArray, allocator, 433 &rba, cursorPosition, moduleCache); 434 scope(exit) pair.destroy(); 435 response.setCompletions(pair.scope_, getExpression(beforeTokens), 436 cursorPosition, CompletionType.identifiers, false, partial); 437 break; 438 case tok!"(": 439 case tok!"{": 440 case tok!"[": 441 case tok!";": 442 case tok!":": 443 break; 444 default: 445 break; 446 } 447 return response; 448 } 449 450 /** 451 * Params: 452 * sourceCode = the source code of the file being edited 453 * cursorPosition = the cursor position in bytes 454 * Returns: 455 * a sorted range of tokens before the cursor position 456 */ 457 auto getTokensBeforeCursor(const(ubyte[]) sourceCode, size_t cursorPosition, 458 ref StringCache cache, out const(Token)[] tokenArray) 459 { 460 LexerConfig config; 461 config.fileName = ""; 462 tokenArray = getTokensForParser(cast(ubyte[]) sourceCode, config, &cache); 463 auto sortedTokens = assumeSorted(tokenArray); 464 return sortedTokens.lowerBound(cast(size_t) cursorPosition); 465 } 466 467 /** 468 * Params: 469 * request = the autocompletion request 470 * type = type the autocompletion type 471 * Returns: 472 * all symbols that should be considered for the autocomplete list based on 473 * the request's source code, cursor position, and completion type. 474 */ 475 SymbolStuff getSymbolsForCompletion(const AutocompleteRequest request, 476 const CompletionType type, IAllocator allocator, RollbackAllocator* rba, 477 ref StringCache cache, ref ModuleCache moduleCache) 478 { 479 const(Token)[] tokenArray; 480 auto beforeTokens = getTokensBeforeCursor(request.sourceCode, 481 request.cursorPosition, cache, tokenArray); 482 ScopeSymbolPair pair = generateAutocompleteTrees(tokenArray, allocator, 483 rba, request.cursorPosition, moduleCache); 484 auto expression = getExpression(beforeTokens); 485 return SymbolStuff(getSymbolsByTokenChain(pair.scope_, expression, 486 request.cursorPosition, type), pair.symbol, pair.scope_); 487 } 488 489 struct SymbolStuff 490 { 491 void destroy() 492 { 493 typeid(DSymbol).destroy(symbol); 494 typeid(Scope).destroy(scope_); 495 } 496 497 DSymbol*[] symbols; 498 DSymbol* symbol; 499 Scope* scope_; 500 } 501 502 /** 503 * Handles paren completion for function calls and some keywords 504 * Params: 505 * beforeTokens = the tokens before the cursor 506 * tokenArray = all tokens in the file 507 * cursorPosition = the cursor position in bytes 508 * Returns: 509 * the autocompletion response 510 */ 511 AutocompleteResponse parenCompletion(T)(T beforeTokens, 512 const(Token)[] tokenArray, size_t cursorPosition, ref ModuleCache moduleCache) 513 { 514 AutocompleteResponse response; 515 immutable(string)[] completions; 516 switch (beforeTokens[$ - 2].type) 517 { 518 case tok!"__traits": 519 completions = traits; 520 goto fillResponse; 521 case tok!"scope": 522 completions = scopes; 523 goto fillResponse; 524 case tok!"version": 525 completions = predefinedVersions; 526 goto fillResponse; 527 case tok!"extern": 528 completions = linkages; 529 goto fillResponse; 530 case tok!"pragma": 531 completions = pragmas; 532 fillResponse: 533 response.completionType = CompletionType.identifiers; 534 foreach (completion; completions) 535 { 536 response.completions ~= completion; 537 response.completionKinds ~= CompletionKind.keyword; 538 } 539 break; 540 case tok!"characterLiteral": 541 case tok!"doubleLiteral": 542 case tok!"dstringLiteral": 543 case tok!"floatLiteral": 544 case tok!"identifier": 545 case tok!"idoubleLiteral": 546 case tok!"ifloatLiteral": 547 case tok!"intLiteral": 548 case tok!"irealLiteral": 549 case tok!"longLiteral": 550 case tok!"realLiteral": 551 case tok!"stringLiteral": 552 case tok!"uintLiteral": 553 case tok!"ulongLiteral": 554 case tok!"wstringLiteral": 555 case tok!"this": 556 case tok!"super": 557 case tok!")": 558 case tok!"]": 559 auto allocator = scoped!(ASTAllocator)(); 560 RollbackAllocator rba; 561 ScopeSymbolPair pair = generateAutocompleteTrees(tokenArray, allocator, 562 &rba, cursorPosition, moduleCache); 563 scope(exit) pair.destroy(); 564 auto expression = getExpression(beforeTokens[0 .. $ - 1]); 565 response.setCompletions(pair.scope_, expression, 566 cursorPosition, CompletionType.calltips, beforeTokens[$ - 1] == tok!"["); 567 break; 568 default: 569 break; 570 } 571 return response; 572 } 573 574 /** 575 * Determines if an import is selective, whole-module, or neither. 576 */ 577 ImportKind determineImportKind(T)(T tokens) 578 { 579 assert (tokens.length > 1); 580 size_t i = tokens.length - 1; 581 if (!(tokens[i] == tok!":" || tokens[i] == tok!"," || tokens[i] == tok!"." 582 || tokens[i] == tok!"identifier")) 583 return ImportKind.neither; 584 bool foundColon = false; 585 while (true) switch (tokens[i].type) 586 { 587 case tok!":": 588 foundColon = true; 589 goto case; 590 case tok!"identifier": 591 case tok!"=": 592 case tok!".": 593 case tok!",": 594 if (i == 0) 595 return ImportKind.neither; 596 else 597 i--; 598 break; 599 case tok!"import": 600 return foundColon ? ImportKind.selective : ImportKind.normal; 601 default: 602 return ImportKind.neither; 603 } 604 return ImportKind.neither; 605 } 606 607 unittest 608 { 609 import std.stdio : writeln; 610 611 Token[] t = [ 612 Token(tok!"import"), Token(tok!"identifier"), Token(tok!"."), 613 Token(tok!"identifier"), Token(tok!":"), Token(tok!"identifier"), Token(tok!",") 614 ]; 615 assert(determineImportKind(t) == ImportKind.selective); 616 Token[] t2; 617 t2 ~= Token(tok!"else"); 618 t2 ~= Token(tok!":"); 619 assert(determineImportKind(t2) == ImportKind.neither); 620 writeln("Unittest for determineImportKind() passed"); 621 } 622 623 /** 624 * Provides autocomplete for selective imports, e.g.: 625 * --- 626 * import std.algorithm: balancedParens; 627 * --- 628 */ 629 AutocompleteResponse importCompletion(T)(T beforeTokens, ImportKind kind, 630 ref ModuleCache moduleCache) 631 in 632 { 633 assert (beforeTokens.length >= 2); 634 } 635 body 636 { 637 AutocompleteResponse response; 638 if (beforeTokens.length <= 2) 639 return response; 640 641 size_t i = beforeTokens.length - 1; 642 643 if (kind == ImportKind.normal) 644 { 645 646 while (beforeTokens[i].type != tok!"," && beforeTokens[i].type != tok!"import" 647 && beforeTokens[i].type != tok!"=" ) 648 i--; 649 setImportCompletions(beforeTokens[i .. $], response, moduleCache); 650 return response; 651 } 652 653 loop: while (true) switch (beforeTokens[i].type) 654 { 655 case tok!"identifier": 656 case tok!"=": 657 case tok!",": 658 case tok!".": 659 i--; 660 break; 661 case tok!":": 662 i--; 663 while (beforeTokens[i].type == tok!"identifier" || beforeTokens[i].type == tok!".") 664 i--; 665 break loop; 666 default: 667 break loop; 668 } 669 670 size_t j = i; 671 loop2: while (j <= beforeTokens.length) switch (beforeTokens[j].type) 672 { 673 case tok!":": break loop2; 674 default: j++; break; 675 } 676 677 if (i >= j) 678 { 679 warning("Malformed import statement"); 680 return response; 681 } 682 683 immutable string path = beforeTokens[i + 1 .. j] 684 .filter!(token => token.type == tok!"identifier") 685 .map!(token => cast() token.text) 686 .joiner(dirSeparator) 687 .text(); 688 689 string resolvedLocation = moduleCache.resolveImportLocation(path); 690 if (resolvedLocation is null) 691 { 692 warning("Could not resolve location of ", path); 693 return response; 694 } 695 auto symbols = moduleCache.getModuleSymbol(internString(resolvedLocation)); 696 697 import containers.hashset : HashSet; 698 HashSet!string h; 699 700 void addSymbolToResponses(const(DSymbol)* sy) 701 { 702 auto a = DSymbol(sy.name); 703 if (!builtinSymbols.contains(&a) && sy.name !is null && !h.contains(sy.name) 704 && !sy.skipOver && sy.name != CONSTRUCTOR_SYMBOL_NAME 705 && isPublicCompletionKind(sy.kind)) 706 { 707 response.completionKinds ~= sy.kind; 708 response.completions ~= sy.name; 709 h.insert(sy.name); 710 } 711 } 712 713 foreach (s; symbols.opSlice().filter!(a => !a.skipOver)) 714 { 715 if (s.kind == CompletionKind.importSymbol && s.type !is null) 716 foreach (sy; s.type.opSlice().filter!(a => !a.skipOver)) 717 addSymbolToResponses(sy); 718 else 719 addSymbolToResponses(s); 720 } 721 response.completionType = CompletionType.identifiers; 722 return response; 723 } 724 725 /** 726 * Populates the response with completion information for an import statement 727 * Params: 728 * tokens = the tokens after the "import" keyword and before the cursor 729 * response = the response that should be populated 730 */ 731 void setImportCompletions(T)(T tokens, ref AutocompleteResponse response, 732 ref ModuleCache cache) 733 { 734 response.completionType = CompletionType.identifiers; 735 string partial = null; 736 if (tokens[$ - 1].type == tok!"identifier") 737 { 738 partial = tokens[$ - 1].text; 739 tokens = tokens[0 .. $ - 1]; 740 } 741 auto moduleParts = tokens.filter!(a => a.type == tok!"identifier").map!("a.text").array(); 742 string path = buildPath(moduleParts); 743 744 bool found = false; 745 746 foreach (importPath; cache.getImportPaths()) 747 { 748 if (importPath.isFile) 749 { 750 if (!exists(importPath)) 751 continue; 752 753 found = true; 754 755 auto n = importPath.baseName(".d").baseName(".di"); 756 if (isFile(importPath) && (importPath.endsWith(".d") || importPath.endsWith(".di")) 757 && (partial is null || n.startsWith(partial))) 758 { 759 response.completions ~= n; 760 response.completionKinds ~= CompletionKind.moduleName; 761 } 762 } 763 else 764 { 765 string p = buildPath(importPath, path); 766 if (!exists(p)) 767 continue; 768 769 found = true; 770 771 try foreach (string name; dirEntries(p, SpanMode.shallow)) 772 { 773 import std.path: baseName; 774 if (name.baseName.startsWith(".#")) 775 continue; 776 777 auto n = name.baseName(".d").baseName(".di"); 778 if (isFile(name) && (name.endsWith(".d") || name.endsWith(".di")) 779 && (partial is null || n.startsWith(partial))) 780 { 781 response.completions ~= n; 782 response.completionKinds ~= CompletionKind.moduleName; 783 } 784 else if (isDir(name)) 785 { 786 if (n[0] != '.' && (partial is null || n.startsWith(partial))) 787 { 788 response.completions ~= n; 789 response.completionKinds ~= 790 exists(buildPath(name, "package.d")) || exists(buildPath(name, "package.di")) 791 ? CompletionKind.moduleName : CompletionKind.packageName; 792 } 793 } 794 } 795 catch(FileException) 796 { 797 warning("Cannot access import path: ", importPath); 798 } 799 } 800 } 801 if (!found) 802 warning("Could not find ", moduleParts); 803 } 804 805 static void skip(alias O, alias C, T)(T t, ref size_t i) 806 { 807 int depth = 1; 808 while (i < t.length) switch (t[i].type) 809 { 810 case O: 811 i++; 812 depth++; 813 break; 814 case C: 815 i++; 816 depth--; 817 if (depth <= 0) 818 return; 819 break; 820 default: 821 i++; 822 break; 823 } 824 } 825 826 bool isSliceExpression(T)(T tokens, size_t index) 827 { 828 while (index < tokens.length) switch (tokens[index].type) 829 { 830 case tok!"[": 831 skip!(tok!"[", tok!"]")(tokens, index); 832 break; 833 case tok!"(": 834 skip!(tok!"(", tok!")")(tokens, index); 835 break; 836 case tok!"]": 837 case tok!"}": 838 return false; 839 case tok!"..": 840 return true; 841 default: 842 index++; 843 break; 844 } 845 return false; 846 } 847 848 /** 849 * 850 */ 851 DSymbol*[] getSymbolsByTokenChain(T)(Scope* completionScope, 852 T tokens, size_t cursorPosition, CompletionType completionType) 853 { 854 //writeln(">>>"); 855 //dumpTokens(tokens.release); 856 //writeln(">>>"); 857 858 static size_t skipEnd(T tokenSlice, size_t i, IdType open, IdType close) 859 { 860 size_t j = i + 1; 861 for (int depth = 1; depth > 0 && j < tokenSlice.length; j++) 862 { 863 if (tokenSlice[j].type == open) 864 depth++; 865 else if (tokenSlice[j].type == close) 866 { 867 depth--; 868 if (depth == 0) break; 869 } 870 } 871 return j; 872 } 873 874 // Find the symbol corresponding to the beginning of the chain 875 DSymbol*[] symbols; 876 if (tokens.length == 0) 877 return []; 878 // Recurse in case the symbol chain starts with an expression in parens 879 // e.g. (a.b!c).d 880 if (tokens[0] == tok!"(") 881 { 882 immutable j = skipEnd(tokens, 0, tok!"(", tok!")"); 883 symbols = getSymbolsByTokenChain(completionScope, tokens[1 .. j], 884 cursorPosition, completionType); 885 tokens = tokens[j + 1 .. $]; 886 //writeln("<<<"); 887 //dumpTokens(tokens.release); 888 //writeln("<<<"); 889 if (tokens.length == 0) // workaround (#371) 890 return []; 891 } 892 else if (tokens[0] == tok!"." && tokens.length > 1) 893 { 894 tokens = tokens[1 .. $]; 895 if (tokens.length == 0) // workaround (#371) 896 return []; 897 symbols = completionScope.getSymbolsAtGlobalScope(stringToken(tokens[0])); 898 } 899 else 900 symbols = completionScope.getSymbolsByNameAndCursor(stringToken(tokens[0]), cursorPosition); 901 902 if (symbols.length == 0) 903 { 904 //TODO: better bugfix for issue #368, see test case 52 or pull #371 905 if (tokens.length) 906 warning("Could not find declaration of ", stringToken(tokens[0]), 907 " from position ", cursorPosition); 908 else assert(0, "internal error"); 909 return []; 910 } 911 912 // If the `symbols` array contains functions, and one of them returns 913 // void and the others do not, this is a property function. For the 914 // purposes of chaining auto-complete we want to ignore the one that 915 // returns void. This is a no-op if we are getting doc comments. 916 void filterProperties() @nogc @safe 917 { 918 if (symbols.length == 0 || completionType == CompletionType.ddoc) 919 return; 920 if (symbols[0].kind == CompletionKind.functionName 921 || symbols[0].qualifier == SymbolQualifier.func) 922 { 923 int voidRets = 0; 924 int nonVoidRets = 0; 925 size_t firstNonVoidIndex = size_t.max; 926 foreach (i, sym; symbols) 927 { 928 if (sym.type is null) 929 return; 930 if (&sym.type.name[0] == &getBuiltinTypeName(tok!"void")[0]) 931 voidRets++; 932 else 933 { 934 nonVoidRets++; 935 firstNonVoidIndex = min(firstNonVoidIndex, i); 936 } 937 } 938 if (voidRets > 0 && nonVoidRets > 0) 939 symbols = symbols[firstNonVoidIndex .. $]; 940 } 941 } 942 943 filterProperties(); 944 945 if (shouldSwapWithType(completionType, symbols[0].kind, 0, tokens.length - 1)) 946 { 947 //trace("Swapping types"); 948 if (symbols.length == 0 || symbols[0].type is null || symbols[0].type is symbols[0]) 949 return []; 950 else if (symbols[0].type.kind == CompletionKind.functionName) 951 { 952 if (symbols[0].type.type is null) 953 symbols = []; 954 else 955 symbols = [symbols[0].type.type]; 956 } 957 else 958 symbols = [symbols[0].type]; 959 } 960 961 loop: for (size_t i = 1; i < tokens.length; i++) 962 { 963 void skip(IdType open, IdType close) 964 { 965 i = skipEnd(tokens, i, open, close); 966 } 967 968 switch (tokens[i].type) 969 { 970 case tok!"int": 971 case tok!"uint": 972 case tok!"long": 973 case tok!"ulong": 974 case tok!"char": 975 case tok!"wchar": 976 case tok!"dchar": 977 case tok!"bool": 978 case tok!"byte": 979 case tok!"ubyte": 980 case tok!"short": 981 case tok!"ushort": 982 case tok!"cent": 983 case tok!"ucent": 984 case tok!"float": 985 case tok!"ifloat": 986 case tok!"cfloat": 987 case tok!"idouble": 988 case tok!"cdouble": 989 case tok!"double": 990 case tok!"real": 991 case tok!"ireal": 992 case tok!"creal": 993 case tok!"this": 994 case tok!"super": 995 symbols = symbols[0].getPartsByName(internString(str(tokens[i].type))); 996 if (symbols.length == 0) 997 break loop; 998 break; 999 case tok!"identifier": 1000 //trace(symbols[0].qualifier, " ", symbols[0].kind); 1001 filterProperties(); 1002 1003 if (symbols.length == 0) 1004 break loop; 1005 1006 // Use type instead of the symbol itself for certain symbol kinds 1007 while (symbols[0].qualifier == SymbolQualifier.func 1008 || symbols[0].kind == CompletionKind.functionName 1009 || (symbols[0].kind == CompletionKind.moduleName 1010 && symbols[0].type !is null && symbols[0].type.kind == CompletionKind.importSymbol) 1011 || symbols[0].kind == CompletionKind.importSymbol 1012 || symbols[0].kind == CompletionKind.aliasName) 1013 { 1014 symbols = symbols[0].type is null || symbols[0].type is symbols[0] ? [] : [symbols[0].type]; 1015 if (symbols.length == 0) 1016 break loop; 1017 } 1018 1019 //trace("looking for ", tokens[i].text, " in ", symbols[0].name); 1020 symbols = symbols[0].getPartsByName(internString(tokens[i].text)); 1021 //trace("symbols: ", symbols.map!(a => a.name)); 1022 filterProperties(); 1023 if (symbols.length == 0) 1024 { 1025 //trace("Couldn't find it."); 1026 break loop; 1027 } 1028 if (shouldSwapWithType(completionType, symbols[0].kind, i, tokens.length - 1)) 1029 { 1030 symbols = symbols[0].type is null || symbols[0].type is symbols[0] ? [] : [symbols[0].type]; 1031 if (symbols.length == 0) 1032 break loop; 1033 } 1034 if ((symbols[0].kind == CompletionKind.aliasName 1035 || symbols[0].kind == CompletionKind.moduleName) 1036 && (completionType == CompletionType.identifiers 1037 || i + 1 < tokens.length)) 1038 { 1039 symbols = symbols[0].type is null || symbols[0].type is symbols[0] ? [] : [symbols[0].type]; 1040 } 1041 if (symbols.length == 0) 1042 break loop; 1043 if (tokens[i].type == tok!"!") 1044 { 1045 i++; 1046 if (tokens[i].type == tok!"(") 1047 goto case; 1048 else 1049 i++; 1050 } 1051 break; 1052 case tok!"(": 1053 skip(tok!"(", tok!")"); 1054 break; 1055 case tok!"[": 1056 if (symbols[0].qualifier == SymbolQualifier.array) 1057 { 1058 skip(tok!"[", tok!"]"); 1059 if (!isSliceExpression(tokens, i)) 1060 { 1061 symbols = symbols[0].type is null || symbols[0].type is symbols[0] ? [] : [symbols[0].type]; 1062 if (symbols.length == 0) 1063 break loop; 1064 } 1065 } 1066 else if (symbols[0].qualifier == SymbolQualifier.assocArray) 1067 { 1068 symbols = symbols[0].type is null || symbols[0].type is symbols[0] ? [] : [symbols[0].type]; 1069 skip(tok!"[", tok!"]"); 1070 } 1071 else 1072 { 1073 skip(tok!"[", tok!"]"); 1074 DSymbol*[] overloads; 1075 if (isSliceExpression(tokens, i)) 1076 overloads = symbols[0].getPartsByName(internString("opSlice")); 1077 else 1078 overloads = symbols[0].getPartsByName(internString("opIndex")); 1079 if (overloads.length > 0) 1080 { 1081 symbols = overloads[0].type is null ? [] : [overloads[0].type]; 1082 } 1083 else 1084 return []; 1085 } 1086 break; 1087 case tok!".": 1088 break; 1089 default: 1090 break loop; 1091 } 1092 } 1093 return symbols; 1094 } 1095 1096 /** 1097 * 1098 */ 1099 void setCompletions(T)(ref AutocompleteResponse response, 1100 Scope* completionScope, T tokens, size_t cursorPosition, 1101 CompletionType completionType, bool isBracket = false, string partial = null) 1102 { 1103 static void addSymToResponse(const(DSymbol)* s, ref AutocompleteResponse r, string p, 1104 size_t[] circularGuard = []) 1105 { 1106 if (circularGuard.canFind(cast(size_t) s)) 1107 return; 1108 foreach (sym; s.opSlice()) 1109 { 1110 if (sym.name !is null && sym.name.length > 0 && isPublicCompletionKind(sym.kind) 1111 && (p is null ? true : toUpper(sym.name.data).startsWith(toUpper(p))) 1112 && !r.completions.canFind(sym.name) 1113 && sym.name[0] != '*') 1114 { 1115 r.completionKinds ~= sym.kind; 1116 r.completions ~= sym.name.dup; 1117 } 1118 if (sym.kind == CompletionKind.importSymbol && !sym.skipOver && sym.type !is null) 1119 addSymToResponse(sym.type, r, p, circularGuard ~ (cast(size_t) s)); 1120 } 1121 } 1122 1123 // Handle the simple case where we get all symbols in scope and filter it 1124 // based on the currently entered text. 1125 if (partial !is null && tokens.length == 0) 1126 { 1127 auto currentSymbols = completionScope.getSymbolsInCursorScope(cursorPosition); 1128 foreach (s; currentSymbols.filter!(a => isPublicCompletionKind(a.kind) 1129 && toUpper(a.name.data).startsWith(toUpper(partial)))) 1130 { 1131 response.completionKinds ~= s.kind; 1132 response.completions ~= s.name.dup; 1133 } 1134 response.completionType = CompletionType.identifiers; 1135 return; 1136 } 1137 1138 if (tokens.length == 0) 1139 return; 1140 1141 DSymbol*[] symbols = getSymbolsByTokenChain(completionScope, tokens, 1142 cursorPosition, completionType); 1143 1144 if (symbols.length == 0) 1145 return; 1146 1147 if (completionType == CompletionType.identifiers) 1148 { 1149 while (symbols[0].qualifier == SymbolQualifier.func 1150 || symbols[0].kind == CompletionKind.functionName 1151 || symbols[0].kind == CompletionKind.importSymbol 1152 || symbols[0].kind == CompletionKind.aliasName) 1153 { 1154 symbols = symbols[0].type is null || symbols[0].type is symbols[0] ? [] 1155 : [symbols[0].type]; 1156 if (symbols.length == 0) 1157 return; 1158 } 1159 addSymToResponse(symbols[0], response, partial); 1160 response.completionType = CompletionType.identifiers; 1161 } 1162 else if (completionType == CompletionType.calltips) 1163 { 1164 //trace("Showing call tips for ", symbols[0].name, " of kind ", symbols[0].kind); 1165 if (symbols[0].kind != CompletionKind.functionName 1166 && symbols[0].callTip is null) 1167 { 1168 if (symbols[0].kind == CompletionKind.aliasName) 1169 { 1170 if (symbols[0].type is null || symbols[0].type is symbols[0]) 1171 return; 1172 symbols = [symbols[0].type]; 1173 } 1174 if (symbols[0].kind == CompletionKind.variableName) 1175 { 1176 auto dumb = symbols[0].type; 1177 if (dumb !is null) 1178 { 1179 if (dumb.kind == CompletionKind.functionName) 1180 { 1181 symbols = [dumb]; 1182 goto setCallTips; 1183 } 1184 if (isBracket) 1185 { 1186 auto index = dumb.getPartsByName(internString("opIndex")); 1187 if (index.length > 0) 1188 { 1189 symbols = index; 1190 goto setCallTips; 1191 } 1192 } 1193 auto call = dumb.getPartsByName(internString("opCall")); 1194 if (call.length > 0) 1195 { 1196 symbols = call; 1197 goto setCallTips; 1198 } 1199 } 1200 } 1201 if (symbols[0].kind == CompletionKind.structName 1202 || symbols[0].kind == CompletionKind.className) 1203 { 1204 auto constructor = symbols[0].getPartsByName(CONSTRUCTOR_SYMBOL_NAME); 1205 if (constructor.length == 0) 1206 { 1207 // Build a call tip out of the struct fields 1208 if (symbols[0].kind == CompletionKind.structName) 1209 { 1210 response.completionType = CompletionType.calltips; 1211 response.completions = [generateStructConstructorCalltip(symbols[0])]; 1212 return; 1213 } 1214 } 1215 else 1216 { 1217 symbols = constructor; 1218 goto setCallTips; 1219 } 1220 } 1221 } 1222 setCallTips: 1223 response.completionType = CompletionType.calltips; 1224 foreach (symbol; symbols) 1225 { 1226 if (symbol.kind != CompletionKind.aliasName && symbol.callTip !is null) 1227 response.completions ~= symbol.callTip; 1228 } 1229 } 1230 } 1231 1232 string generateStructConstructorCalltip(const DSymbol* symbol) 1233 in 1234 { 1235 assert(symbol.kind == CompletionKind.structName); 1236 } 1237 body 1238 { 1239 string generatedStructConstructorCalltip = "this("; 1240 const(DSymbol)*[] fields = symbol.opSlice().filter!( 1241 a => a.kind == CompletionKind.variableName).map!(a => cast(const(DSymbol)*) a).array(); 1242 fields.sort!((a, b) => a.location < b.location); 1243 foreach (i, field; fields) 1244 { 1245 if (field.kind != CompletionKind.variableName) 1246 continue; 1247 i++; 1248 if (field.type !is null) 1249 { 1250 generatedStructConstructorCalltip ~= field.type.name; 1251 generatedStructConstructorCalltip ~= " "; 1252 } 1253 generatedStructConstructorCalltip ~= field.name; 1254 if (i < fields.length) 1255 generatedStructConstructorCalltip ~= ", "; 1256 } 1257 generatedStructConstructorCalltip ~= ")"; 1258 return generatedStructConstructorCalltip; 1259 } 1260 1261 private enum TYPE_IDENT_AND_LITERAL_CASES = q{ 1262 case tok!"int": 1263 case tok!"uint": 1264 case tok!"long": 1265 case tok!"ulong": 1266 case tok!"char": 1267 case tok!"wchar": 1268 case tok!"dchar": 1269 case tok!"bool": 1270 case tok!"byte": 1271 case tok!"ubyte": 1272 case tok!"short": 1273 case tok!"ushort": 1274 case tok!"cent": 1275 case tok!"ucent": 1276 case tok!"float": 1277 case tok!"ifloat": 1278 case tok!"cfloat": 1279 case tok!"idouble": 1280 case tok!"cdouble": 1281 case tok!"double": 1282 case tok!"real": 1283 case tok!"ireal": 1284 case tok!"creal": 1285 case tok!"this": 1286 case tok!"super": 1287 case tok!"identifier": 1288 case tok!"stringLiteral": 1289 case tok!"wstringLiteral": 1290 case tok!"dstringLiteral": 1291 }; 1292 1293 bool isUdaExpression(T)(ref T tokens) 1294 { 1295 bool result; 1296 ptrdiff_t skip; 1297 ptrdiff_t i = tokens.length - 2; 1298 1299 if (i < 1) 1300 return result; 1301 1302 // skips the UDA ctor 1303 if (tokens[i].type == tok!")") 1304 { 1305 ++skip; 1306 --i; 1307 while (i >= 2) 1308 { 1309 skip += tokens[i].type == tok!")"; 1310 skip -= tokens[i].type == tok!"("; 1311 --i; 1312 if (skip == 0) 1313 { 1314 // @UDA!(TemplateParameters)(FunctionParameters) 1315 if (i > 3 && tokens[i].type == tok!"!" && tokens[i-1].type == tok!")") 1316 { 1317 skip = 1; 1318 i -= 2; 1319 continue; 1320 } 1321 else break; 1322 } 1323 } 1324 } 1325 1326 if (skip == 0) 1327 { 1328 // @UDA!SingleTemplateParameter 1329 if (i > 2 && tokens[i].type == tok!"identifier" && tokens[i-1].type == tok!"!") 1330 { 1331 i -= 2; 1332 } 1333 1334 // @UDA 1335 if (i > 0 && tokens[i].type == tok!"identifier" && tokens[i-1].type == tok!"@") 1336 { 1337 result = true; 1338 } 1339 } 1340 1341 return result; 1342 } 1343 1344 /** 1345 * 1346 */ 1347 T getExpression(T)(T beforeTokens) 1348 { 1349 enum EXPRESSION_LOOP_BREAK = q{ 1350 if (i + 1 < beforeTokens.length) switch (beforeTokens[i + 1].type) 1351 { 1352 mixin (TYPE_IDENT_AND_LITERAL_CASES); 1353 i++; 1354 break expressionLoop; 1355 default: 1356 break; 1357 } 1358 }; 1359 1360 if (beforeTokens.length == 0) 1361 return beforeTokens[0 .. 0]; 1362 size_t i = beforeTokens.length - 1; 1363 size_t sliceEnd = beforeTokens.length; 1364 IdType open; 1365 IdType close; 1366 uint skipCount = 0; 1367 1368 expressionLoop: while (true) 1369 { 1370 switch (beforeTokens[i].type) 1371 { 1372 case tok!"import": 1373 i++; 1374 break expressionLoop; 1375 mixin (TYPE_IDENT_AND_LITERAL_CASES); 1376 mixin (EXPRESSION_LOOP_BREAK); 1377 break; 1378 case tok!".": 1379 break; 1380 case tok!")": 1381 open = tok!")"; 1382 close = tok!"("; 1383 goto skip; 1384 case tok!"]": 1385 open = tok!"]"; 1386 close = tok!"["; 1387 skip: 1388 mixin (EXPRESSION_LOOP_BREAK); 1389 immutable bookmark = i; 1390 int depth = 1; 1391 do 1392 { 1393 if (depth == 0 || i == 0) 1394 break; 1395 else 1396 i--; 1397 if (beforeTokens[i].type == open) 1398 depth++; 1399 else if (beforeTokens[i].type == close) 1400 depth--; 1401 } while (true); 1402 1403 skipCount++; 1404 1405 // check the current token after skipping parens to the left. 1406 // if it's a loop keyword, pretend we never skipped the parens. 1407 if (i > 0) switch (beforeTokens[i - 1].type) 1408 { 1409 case tok!"scope": 1410 case tok!"if": 1411 case tok!"while": 1412 case tok!"for": 1413 case tok!"foreach": 1414 case tok!"foreach_reverse": 1415 case tok!"do": 1416 case tok!"cast": 1417 case tok!"catch": 1418 i = bookmark + 1; 1419 break expressionLoop; 1420 case tok!"!": 1421 if (skipCount == 1) 1422 { 1423 sliceEnd = i - 1; 1424 i -= 2; 1425 } 1426 break expressionLoop; 1427 default: 1428 break; 1429 } 1430 break; 1431 default: 1432 i++; 1433 break expressionLoop; 1434 } 1435 if (i == 0) 1436 break; 1437 else 1438 i--; 1439 } 1440 return beforeTokens[i .. sliceEnd]; 1441 } 1442 1443 size_t goBackToOpenParen(T)(T beforeTokens) 1444 in 1445 { 1446 assert (beforeTokens.length > 0); 1447 } 1448 body 1449 { 1450 size_t i = beforeTokens.length - 1; 1451 IdType open; 1452 IdType close; 1453 while (true) switch (beforeTokens[i].type) 1454 { 1455 case tok!",": 1456 case tok!".": 1457 case tok!"*": 1458 case tok!"&": 1459 case tok!"doubleLiteral": 1460 case tok!"floatLiteral": 1461 case tok!"idoubleLiteral": 1462 case tok!"ifloatLiteral": 1463 case tok!"intLiteral": 1464 case tok!"longLiteral": 1465 case tok!"realLiteral": 1466 case tok!"irealLiteral": 1467 case tok!"uintLiteral": 1468 case tok!"ulongLiteral": 1469 case tok!"characterLiteral": 1470 mixin(TYPE_IDENT_AND_LITERAL_CASES); 1471 if (i == 0) 1472 return size_t.max; 1473 else 1474 i--; 1475 break; 1476 case tok!"(": 1477 case tok!"[": 1478 return i + 1; 1479 case tok!")": 1480 open = tok!")"; 1481 close = tok!"("; 1482 goto skip; 1483 case tok!"}": 1484 open = tok!"}"; 1485 close = tok!"{"; 1486 goto skip; 1487 case tok!"]": 1488 open = tok!"]"; 1489 close = tok!"["; 1490 skip: 1491 if (i == 0) 1492 return size_t.max; 1493 else 1494 i--; 1495 int depth = 1; 1496 do 1497 { 1498 if (depth == 0 || i == 0) 1499 break; 1500 else 1501 i--; 1502 if (beforeTokens[i].type == open) 1503 depth++; 1504 else if (beforeTokens[i].type == close) 1505 depth--; 1506 } while (true); 1507 break; 1508 default: 1509 return size_t.max; 1510 } 1511 return size_t.max; 1512 } 1513 1514 /** 1515 * Params: 1516 * completionType = the completion type being requested 1517 * kind = the kind of the current item in the completion chain 1518 * current = the index of the current item in the symbol chain 1519 * max = the number of items in the symbol chain 1520 * Returns: 1521 * true if the symbol should be swapped with its type field 1522 */ 1523 bool shouldSwapWithType(CompletionType completionType, CompletionKind kind, 1524 size_t current, size_t max) pure nothrow @safe 1525 { 1526 // packages never have types, so always return false 1527 if (kind == CompletionKind.packageName 1528 || kind == CompletionKind.className 1529 || kind == CompletionKind.structName 1530 || kind == CompletionKind.interfaceName 1531 || kind == CompletionKind.enumName 1532 || kind == CompletionKind.unionName 1533 || kind == CompletionKind.templateName 1534 || kind == CompletionKind.keyword) 1535 { 1536 return false; 1537 } 1538 // Swap out every part of a chain with its type except the last part 1539 if (current < max) 1540 return true; 1541 // Only swap out types for these kinds 1542 immutable bool isInteresting = 1543 kind == CompletionKind.variableName 1544 || kind == CompletionKind.memberVariableName 1545 || kind == CompletionKind.importSymbol 1546 || kind == CompletionKind.aliasName 1547 || kind == CompletionKind.enumMember 1548 || kind == CompletionKind.functionName; 1549 return isInteresting && (completionType == CompletionType.identifiers 1550 || (completionType == completionType.calltips && kind == CompletionKind.variableName)) ; 1551 } 1552 1553 istring stringToken()(auto ref const Token a) 1554 { 1555 return internString(a.text is null ? str(a.type) : a.text); 1556 } 1557 1558 //void dumpTokens(const Token[] tokens) 1559 //{ 1560 //foreach (t; tokens) 1561 //writeln(t.line, ":", t.column, " ", stringToken(t)); 1562 //}