1 module workspaced.com.dcd; 2 3 import std.file : tempDir; 4 5 import core.thread; 6 import std.algorithm; 7 import std.array; 8 import std.ascii; 9 import std.conv; 10 import std.datetime; 11 import std.experimental.logger : trace; 12 import std.json; 13 import std.path; 14 import std.process; 15 import std.random; 16 import std.stdio; 17 import std.string; 18 import std.typecons; 19 20 import workspaced.api; 21 import workspaced.helpers; 22 import workspaced.com.dcd_version; 23 24 import served.dcd.client; 25 26 @component("dcd") 27 class DCDComponent : ComponentWrapper 28 { 29 mixin DefaultComponentWrapper; 30 31 enum WarningId 32 { 33 dcdServerCrash, 34 dcdOutdated, 35 unimplemented 36 } 37 38 enum latestKnownVersion = latestKnownDCDVersion; 39 void load() 40 { 41 installedVersion = workspaced.globalConfiguration.get("dcd", "_installedVersion", ""); 42 43 if (installedVersion.length 44 && this.clientPath == workspaced.globalConfiguration.get("dcd", "_clientPath", "") 45 && this.serverPath == workspaced.globalConfiguration.get("dcd", "_serverPath", "")) 46 { 47 if (workspaced.globalConfiguration.get("dcd", "_usingInternal", false)) 48 client = new BuiltinDCDClient(); 49 else 50 client = new ExternalDCDClient(this.clientPath); 51 trace("Reusing previously identified DCD ", installedVersion); 52 } 53 else 54 { 55 reloadBinaries(); 56 } 57 } 58 59 void reloadBinaries() 60 { 61 string clientPath = this.clientPath; 62 string serverPath = this.serverPath; 63 64 client = null; 65 66 installedVersion = serverPath.getVersionAndFixPath; 67 string serverPathInfo = serverPath != "dcd-server" ? "(" ~ serverPath ~ ") " : ""; 68 trace("Detected dcd-server ", serverPathInfo, installedVersion); 69 70 if (!checkVersion(installedVersion, BuiltinDCDClient.minSupportedServerInclusive) 71 || checkVersion(installedVersion, BuiltinDCDClient.maxSupportedServerExclusive)) 72 { 73 trace("Using dcd-client instead of internal workspace-d client"); 74 75 string clientInstalledVersion = clientPath.getVersionAndFixPath; 76 string clientPathInfo = clientPath != "dcd-client" ? "(" ~ clientPath ~ ") " : ""; 77 trace("Detected dcd-client ", clientPathInfo, clientInstalledVersion); 78 79 if (clientInstalledVersion != installedVersion) 80 throw new Exception("client & server version mismatch"); 81 82 client = new ExternalDCDClient(clientPath); 83 } 84 else 85 { 86 trace("using builtin DCD client"); 87 client = new BuiltinDCDClient(); 88 } 89 90 config.set("dcd", "clientPath", clientPath); 91 config.set("dcd", "serverPath", serverPath); 92 93 assert(this.clientPath == clientPath); 94 assert(this.serverPath == serverPath); 95 96 //dfmt off 97 if (isOutdated) 98 workspaced.messageHandler.warn(refInstance, "dcd", 99 WarningId.dcdOutdated, "DCD is outdated"); 100 //dfmt on 101 102 workspaced.globalConfiguration.set("dcd", "_usingInternal", 103 cast(ExternalDCDClient) client ? false : true); 104 workspaced.globalConfiguration.set("dcd", "_clientPath", clientPath); 105 workspaced.globalConfiguration.set("dcd", "_serverPath", serverPath); 106 workspaced.globalConfiguration.set("dcd", "_installedVersion", installedVersion); 107 } 108 109 /// Returns: true if DCD version is less than latestKnownVersion or if server and client mismatch or if it doesn't exist. 110 bool isOutdated() 111 { 112 if (!installedVersion) 113 { 114 string serverPath = this.serverPath; 115 116 try 117 { 118 installedVersion = serverPath.getVersionAndFixPath; 119 } 120 catch (ProcessException) 121 { 122 return true; 123 } 124 } 125 126 if (installedVersion.isLocallyCompiledDCD) 127 return false; 128 129 return !checkVersion(installedVersion, latestKnownVersion); 130 } 131 132 /// Returns: The current detected installed version of dcd-client. 133 /// Ends with `"-workspaced-builtin"` if this is using the builtin 134 /// client. 135 string clientInstalledVersion() @property const 136 { 137 return cast(ExternalDCDClient) client ? installedVersion : 138 BuiltinDCDClient.clientVersion ~ "-workspaced-builtin"; 139 } 140 141 /// Returns: The current detected installed version of dcd-server. `null` if 142 /// none is installed. 143 string serverInstalledVersion() const 144 { 145 if (!installedVersion) 146 { 147 string serverPath = this.serverPath; 148 149 try 150 { 151 return serverPath.getVersionAndFixPath; 152 } 153 catch (ProcessException) 154 { 155 return null; 156 } 157 } 158 159 return installedVersion; 160 } 161 162 private auto serverThreads() 163 { 164 return threads(1, 2); 165 } 166 167 /// This stops the dcd-server instance safely and waits for it to exit 168 override void shutdown(bool dtor = false) 169 { 170 stopServerSync(); 171 if (!dtor && _threads) 172 serverThreads.finish(); 173 } 174 175 /// This will start the dcd-server and load import paths from the current provider 176 void setupServer(string[] additionalImports = [], bool quietServer = false) 177 { 178 startServer(importPaths ~ importFiles ~ additionalImports, quietServer); 179 } 180 181 /// This will start the dcd-server. If DCD does not support IPC sockets on 182 /// this platform, will use the TCP port specified with the `port` property 183 /// or init config. 184 /// 185 /// Throws an exception if a TCP port is used and another server is already 186 /// running on it. 187 /// 188 /// Params: 189 /// additionalImports = import paths to cache on the server on startup. 190 /// quietServer = if true: no output from DCD server is processed, 191 /// if false: every line will be traced to the output. 192 /// selectPort = if true, increment port until an open one is found 193 /// instead of throwing an exception. 194 void startServer(string[] additionalImports = [], bool quietServer = false, bool selectPort = false) 195 { 196 ushort port = this.port; 197 while (port + 1 < ushort.max && isPortRunning(port)) 198 { 199 if (selectPort) 200 port++; 201 else 202 throw new Exception("Already running dcd on port " ~ port.to!string); 203 } 204 string[] imports; 205 foreach (i; additionalImports) 206 if (i.length) 207 imports ~= "-I" ~ i; 208 209 client.runningPort = port; 210 client.socketFile = buildPath(tempDir, 211 "workspace-d-sock" ~ thisProcessID.to!string ~ "-" ~ uniform!ulong.to!string(36)); 212 213 string[] serverArgs; 214 static if (platformSupportsDCDUnixSockets) 215 serverArgs = [serverPath, "--socketFile", client.socketFile]; 216 else 217 serverArgs = [serverPath, "--port", client.runningPort.to!string]; 218 219 trace("Start dcd-server ", serverArgs); 220 serverPipes = raw(serverArgs ~ imports, 221 Redirect.stdin | Redirect.stderr | Redirect.stdoutToStderr); 222 while (!serverPipes.stderr.eof) 223 { 224 string line = serverPipes.stderr.readln(); 225 if (!quietServer) 226 trace("Server: ", line); 227 if (line.canFind("Startup completed in ")) 228 break; 229 } 230 running = true; 231 serverThreads.create({ 232 mixin(traceTask); 233 scope (exit) 234 running = false; 235 236 try 237 { 238 if (quietServer) 239 foreach (block; serverPipes.stderr.byChunk(4096)) 240 { 241 } 242 else 243 while (serverPipes.stderr.isOpen && !serverPipes.stderr.eof) 244 { 245 auto line = serverPipes.stderr.readln(); 246 trace("Server: ", line); // evaluates lazily, so read before 247 } 248 } 249 catch (Exception e) 250 { 251 workspaced.messageHandler.error(refInstance, "dcd", 252 WarningId.dcdServerCrash, 253 "Reading/clearing stderr from dcd-server crashed (-> killing dcd-server): ", 254 e.toString); 255 serverPipes.pid.kill(); 256 } 257 258 auto code = serverPipes.pid.wait(); 259 trace("DCD-Server stopped with code ", code); 260 if (code != 0) 261 { 262 workspaced.messageHandler.handleCrash(refInstance, "dcd", this); 263 } 264 }); 265 } 266 267 void stopServerSync() 268 { 269 if (!running) 270 return; 271 int i = 0; 272 running = false; 273 client.shutdown(); 274 while (serverPipes.pid && !serverPipes.pid.tryWait().terminated) 275 { 276 Thread.sleep(10.msecs); 277 if (++i > 200) // Kill after 2 seconds 278 { 279 killServer(); 280 return; 281 } 282 } 283 } 284 285 /// This stops the dcd-server asynchronously 286 /// Returns: null 287 Future!void stopServer() 288 { 289 auto ret = new typeof(return)(); 290 gthreads.create({ 291 mixin(traceTask); 292 try 293 { 294 stopServerSync(); 295 ret.finish(); 296 } 297 catch (Throwable t) 298 { 299 ret.error(t); 300 } 301 }); 302 return ret; 303 } 304 305 /// This will kill the process associated with the dcd-server instance 306 void killServer() 307 { 308 if (serverPipes.pid && !serverPipes.pid.tryWait().terminated) 309 serverPipes.pid.kill(); 310 } 311 312 /// This will stop the dcd-server safely and restart it again using setup-server asynchronously 313 /// Returns: null 314 Future!void restartServer(bool quiet = false) 315 { 316 auto ret = new typeof(return); 317 gthreads.create({ 318 mixin(traceTask); 319 try 320 { 321 stopServerSync(); 322 setupServer([], quiet); 323 ret.finish(); 324 } 325 catch (Throwable t) 326 { 327 ret.error(t); 328 } 329 }); 330 return ret; 331 } 332 333 /// This will query the current dcd-server status 334 /// Returns: `{isRunning: bool}` If the dcd-server process is not running 335 /// anymore it will return isRunning: false. Otherwise it will check for 336 /// server status using `dcd-client --query` (or using builtin equivalent) 337 auto serverStatus() @property 338 { 339 DCDServerStatus status; 340 if (serverPipes.pid && serverPipes.pid.tryWait().terminated) 341 status.isRunning = false; 342 else if (client.usingUnixDomainSockets) 343 status.isRunning = true; 344 else 345 status.isRunning = client.queryRunning(); 346 return status; 347 } 348 349 /// Searches for a symbol across all files using `dcd-client --search` 350 Future!(DCDSearchResult[]) searchSymbol(string query) 351 { 352 auto ret = new typeof(return); 353 gthreads.create({ 354 mixin(traceTask); 355 try 356 { 357 if (!running) 358 { 359 ret.finish(null); 360 return; 361 } 362 363 ret.finish(client.requestSymbolSearch(query) 364 .map!(a => DCDSearchResult(a.symbolFilePath, 365 cast(int)a.symbolLocation, [cast(char) a.kind].idup)).array); 366 } 367 catch (Throwable t) 368 { 369 ret.error(t); 370 } 371 }); 372 return ret; 373 } 374 375 /// Reloads import paths from the current provider. Call reload there before calling it here. 376 void refreshImports() 377 { 378 addImports(importPaths ~ importFiles); 379 } 380 381 /// Manually adds import paths as string array 382 void addImports(string[] imports) 383 { 384 imports.sort!"a<b"; 385 knownImports = multiwayUnion([knownImports.filterNonEmpty, imports.filterNonEmpty]).array; 386 updateImports(); 387 } 388 389 /// Manually removes import paths using a string array. Note that trying to 390 /// remove import paths from the import paths provider will result in them 391 /// being readded as soon as refreshImports is called again. 392 void removeImports(string[] imports) 393 { 394 knownImports = setDifference(knownImports, imports.filterNonEmpty).array; 395 updateImports(); 396 } 397 398 string clientPath() @property @ignoredFunc const 399 { 400 return config.get("dcd", "clientPath", "dcd-client"); 401 } 402 403 string serverPath() @property @ignoredFunc const 404 { 405 return config.get("dcd", "serverPath", "dcd-server"); 406 } 407 408 ushort port() @property @ignoredFunc const 409 { 410 return cast(ushort) config.get!int("dcd", "port", 9166); 411 } 412 413 /// Searches for an open port to spawn dcd-server in asynchronously starting with `port`, always increasing by one. 414 /// Returns: 0 if not available, otherwise the port as number 415 Future!ushort findAndSelectPort(ushort port = 9166) 416 { 417 if (client.usingUnixDomainSockets) 418 { 419 return typeof(return).fromResult(0); 420 } 421 auto ret = new typeof(return); 422 gthreads.create({ 423 mixin(traceTask); 424 try 425 { 426 auto newPort = findOpen(port); 427 port = newPort; 428 ret.finish(port); 429 } 430 catch (Throwable t) 431 { 432 ret.error(t); 433 } 434 }); 435 return ret; 436 } 437 438 /// Finds the declaration of the symbol at position `pos` in the code 439 Future!DCDDeclaration findDeclaration(scope const(char)[] code, int pos) 440 { 441 auto ret = new typeof(return); 442 gthreads.create({ 443 mixin(traceTask); 444 try 445 { 446 if (!running || pos >= code.length) 447 { 448 ret.finish(DCDDeclaration.init); 449 return; 450 } 451 452 // We need to move by one character on identifier characters to ensure the start character fits. 453 if (!isIdentifierSeparatingChar(code[pos])) 454 pos++; 455 456 auto info = client.requestSymbolInfo(CodeRequest("stdin", code, pos)); 457 ret.finish(DCDDeclaration(info.declarationFilePath, 458 cast(int) info.declarationLocation)); 459 } 460 catch (Throwable t) 461 { 462 ret.error(t); 463 } 464 }); 465 return ret; 466 } 467 468 /// Finds the documentation of the symbol at position `pos` in the code 469 Future!string getDocumentation(scope const(char)[] code, int pos) 470 { 471 auto ret = new typeof(return); 472 gthreads.create({ 473 mixin(traceTask); 474 try 475 { 476 if (!running || pos >= code.length) 477 { 478 ret.finish(""); 479 return; 480 } 481 482 // We need to move by one character on identifier characters to ensure the start character fits. 483 if (!isIdentifierSeparatingChar(code[pos])) 484 pos++; 485 486 auto doc = client.requestDocumentation(CodeRequest("stdin", code, pos)); 487 ret.finish(doc.join("\n")); 488 } 489 catch (Throwable t) 490 { 491 ret.error(t); 492 } 493 }); 494 return ret; 495 } 496 497 /// Finds declaration and usage of the token at position `pos` within the 498 /// current document. 499 Future!DCDLocalUse findLocalUse(scope const(char)[] code, int pos) 500 { 501 auto ret = new typeof(return); 502 gthreads.create({ 503 mixin(traceTask); 504 try 505 { 506 if (!running || pos >= code.length) 507 { 508 ret.finish(DCDLocalUse.init); 509 return; 510 } 511 512 // We need to move by one character on identifier characters to ensure the start character fits. 513 if (!isIdentifierSeparatingChar(code[pos])) 514 pos++; 515 516 auto localUse = client.requestLocalUse(CodeRequest("stdin", code, pos)); 517 ret.finish(DCDLocalUse(localUse)); 518 } 519 catch (Throwable t) 520 { 521 ret.error(t); 522 } 523 }); 524 return ret; 525 } 526 527 /// Returns the used socket file. Only available on OSX, linux and BSD with DCD >= 0.8.0 528 /// Throws an error if not available. 529 string getSocketFile() 530 { 531 if (!client.usingUnixDomainSockets) 532 throw new Exception("Unix domain sockets not supported"); 533 return client.socketFile; 534 } 535 536 /// Returns the used running port. Throws an error if using unix sockets instead 537 ushort getRunningPort() 538 { 539 if (client.usingUnixDomainSockets) 540 throw new Exception("Using unix domain sockets instead of a port"); 541 return client.runningPort; 542 } 543 544 /// Queries for code completion at position `pos` in code 545 /// Raw is anything else than identifiers and calltips which might not be implemented by this point. 546 /// calltips.symbols and identifiers.definition, identifiers.file, identifiers.location and identifiers.documentation are only available with dcd ~master as of now. 547 Future!DCDCompletions listCompletion(scope const(char)[] code, int pos) 548 { 549 auto ret = new typeof(return); 550 gthreads.create({ 551 mixin(traceTask); 552 try 553 { 554 DCDCompletions completions; 555 if (!running) 556 { 557 trace("DCD not yet running!"); 558 ret.finish(completions); 559 return; 560 } 561 562 auto c = client.requestAutocomplete(CodeRequest("stdin", code, pos)); 563 if (c.type == DCDCompletionType.calltips) 564 { 565 completions.type = DCDCompletions.Type.calltips; 566 auto calltips = appender!(string[]); 567 auto symbols = appender!(DCDCompletions.Symbol[]); 568 foreach (item; c.completions) 569 { 570 calltips ~= item.definition; 571 symbols ~= DCDCompletions.Symbol(item.symbolFilePath, 572 cast(int)item.symbolLocation, item.documentation); 573 } 574 completions._calltips = calltips.data; 575 completions._symbols = symbols.data; 576 } 577 else if (c.type == DCDCompletionType.identifiers) 578 { 579 completions.type = DCDCompletions.Type.identifiers; 580 auto identifiers = appender!(DCDIdentifier[]); 581 foreach (item; c.completions) 582 { 583 identifiers ~= DCDIdentifier(item.identifier, 584 item.kind == char.init ? "" : [cast(char)item.kind].idup, 585 item.definition, item.symbolFilePath, 586 cast(int)item.symbolLocation, item.documentation); 587 } 588 completions._identifiers = identifiers.data; 589 } 590 else 591 { 592 completions.type = DCDCompletions.Type.raw; 593 workspaced.messageHandler.warn(refInstance, "dcd", 594 WarningId.unimplemented, 595 "Unknown DCD completion type: " ~ c.type.to!string); 596 } 597 ret.finish(completions); 598 } 599 catch (Throwable e) 600 { 601 ret.error(e); 602 } 603 }); 604 return ret; 605 } 606 607 void updateImports() 608 { 609 if (!running) 610 return; 611 612 auto existing = client.listImportPaths(); 613 existing.sort!"a<b"; 614 auto toAdd = setDifference(knownImports, existing); 615 client.addImportPaths(toAdd.array); 616 } 617 618 bool fromRunning(bool supportsFullOutput, string socketFile, ushort runningPort) 619 { 620 if (socketFile.length ? isSocketRunning(socketFile) : isPortRunning(runningPort)) 621 { 622 running = true; 623 client.socketFile = socketFile; 624 client.runningPort = runningPort; 625 return true; 626 } 627 else 628 return false; 629 } 630 631 deprecated("clients without full output support no longer supported") bool getSupportsFullOutput() @property 632 { 633 return true; 634 } 635 636 bool isUsingUnixDomainSockets() @property 637 { 638 return client.usingUnixDomainSockets; 639 } 640 641 bool isActive() @property 642 { 643 return running; 644 } 645 646 private: 647 string installedVersion; 648 bool running = false; 649 ProcessPipes serverPipes; 650 string[] knownImports; 651 IDCDClient client = new NullDCDClient(); 652 653 auto raw(string[] args, Redirect redirect = Redirect.all) 654 { 655 return pipeProcess(args, redirect, null, Config.none, refInstance ? instance.cwd : null); 656 } 657 658 auto rawExec(string[] args) 659 { 660 return execute(args, null, Config.none, size_t.max, refInstance ? instance.cwd : null); 661 } 662 663 bool isSocketRunning(string socket) 664 { 665 static if (!platformSupportsDCDUnixSockets) 666 return false; 667 else 668 return isDCDServerRunning(false, socket, 0); 669 } 670 671 bool isPortRunning(ushort port) 672 { 673 static if (platformSupportsDCDUnixSockets) 674 return false; 675 else 676 return isDCDServerRunning(true, null, port); 677 } 678 679 ushort findOpen(ushort port) 680 { 681 --port; 682 bool isRunning; 683 do 684 { 685 isRunning = isPortRunning(++port); 686 } 687 while (isRunning); 688 return port; 689 } 690 } 691 692 class NullDCDClient : IDCDClient 693 { 694 enum Methods = [ 695 "string socketFile() const @property", 696 "void socketFile(string) @property", 697 "ushort runningPort() const @property", 698 "void runningPort(ushort) @property", 699 "bool usingUnixDomainSockets() const @property", 700 "bool queryRunning()", 701 "bool shutdown()", 702 "bool clearCache()", 703 "bool addImportPaths(string[] importPaths)", 704 "bool removeImportPaths(string[] importPaths)", 705 "string[] listImportPaths()", 706 "SymbolInformation requestSymbolInfo(CodeRequest loc)", 707 "string[] requestDocumentation(CodeRequest loc)", 708 "DCDResponse.Completion[] requestSymbolSearch(string query)", 709 "LocalUse requestLocalUse(CodeRequest loc)", 710 "Completion requestAutocomplete(CodeRequest loc)", 711 ]; 712 713 static foreach (method; Methods) 714 { 715 mixin(method, " { 716 import std.experimental.logger : warningf; 717 warningf(\"Trying to use DCD function %s on uninitialized client!\", __FUNCTION__); 718 static if (!is(typeof(return) == void)) 719 return typeof(return).init; 720 }"); 721 } 722 } 723 724 bool supportsUnixDomainSockets(string ver) 725 { 726 return checkVersion(ver, [0, 8, 0]); 727 } 728 729 unittest 730 { 731 assert(supportsUnixDomainSockets("0.8.0-beta2+9ec55f40a26f6bb3ca95dc9232a239df6ed25c37")); 732 assert(!supportsUnixDomainSockets("0.7.9-beta3")); 733 assert(!supportsUnixDomainSockets("0.7.0")); 734 assert(supportsUnixDomainSockets("v0.9.8 c7ea7e081ed9ad2d85e9f981fd047d7fcdb2cf51")); 735 assert(supportsUnixDomainSockets("1.0.0")); 736 } 737 738 /// Returned by findDeclaration 739 struct DCDDeclaration 740 { 741 string file; 742 int position; 743 } 744 745 /// Returned by listCompletion 746 /// When identifiers: `{type:"identifiers", identifiers:[{identifier:string, type:string, definition:string, file:string, location:number, documentation:string}]}` 747 /// When calltips: `{type:"calltips", calltips:[string], symbols:[{file:string, location:number, documentation:string}]}` 748 /// When raw: `{type:"raw", raw:[string]}` 749 struct DCDCompletions 750 { 751 /// Type of a completion 752 enum Type 753 { 754 /// Unknown/Unimplemented output 755 raw, 756 /// Completion after a dot or a variable name 757 identifiers, 758 /// Completion for arguments in a function call 759 calltips, 760 } 761 762 struct Symbol 763 { 764 string file; 765 int location; 766 string documentation; 767 } 768 769 /// Type of the completion (identifiers, calltips, raw) 770 Type type; 771 deprecated string[] raw; 772 union 773 { 774 DCDIdentifier[] _identifiers; 775 struct 776 { 777 string[] _calltips; 778 Symbol[] _symbols; 779 } 780 } 781 782 enum DCDCompletions empty = DCDCompletions(Type.identifiers); 783 784 /// Only set with type==identifiers. 785 inout(DCDIdentifier[]) identifiers() inout @property 786 { 787 if (type != Type.identifiers) 788 throw new Exception("Type is not identifiers but attempted to access identifiers"); 789 return _identifiers; 790 } 791 792 /// Only set with type==calltips. 793 inout(string[]) calltips() inout @property 794 { 795 if (type != Type.calltips) 796 throw new Exception("Type is not calltips but attempted to access calltips"); 797 return _calltips; 798 } 799 800 /// Only set with type==calltips. 801 inout(Symbol[]) symbols() inout @property 802 { 803 if (type != Type.calltips) 804 throw new Exception("Type is not calltips but attempted to access symbols"); 805 return _symbols; 806 } 807 } 808 809 /// Returned by findLocalUse 810 struct DCDLocalUse 811 { 812 /// File path of the declaration or stdin for input 813 string declarationFilePath; 814 /// Byte location of the declaration inside the declarationFilePath 815 size_t declarationLocation; 816 /// Array of uses within stdin / given document. 817 size_t[] uses; 818 819 this(LocalUse localUse) 820 { 821 foreach (i, ref v; localUse.tupleof) 822 this.tupleof[i] = v; 823 } 824 } 825 826 /// Returned by status 827 struct DCDServerStatus 828 { 829 /// 830 bool isRunning; 831 } 832 833 /// Type of the identifiers value in listCompletion 834 struct DCDIdentifier 835 { 836 /// 837 string identifier; 838 /// 839 string type; 840 /// 841 string definition; 842 /// 843 string file; 844 /// byte location 845 int location; 846 /// 847 string documentation; 848 } 849 850 /// Returned by search-symbol 851 struct DCDSearchResult 852 { 853 /// 854 string file; 855 /// 856 int position; 857 /// 858 string type; 859 } 860 861 private auto filterNonEmpty(T)(T range) 862 { 863 return range.filter!(a => a.length); 864 }