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 dcd.server.autocomplete.complete; 20 21 import std.algorithm; 22 import std.array; 23 import std.conv; 24 import std.experimental.logger; 25 import std.file; 26 import std.path; 27 import std.string; 28 import std.typecons; 29 30 import dcd.server.autocomplete.util; 31 32 import dparse.lexer; 33 import dparse.rollback_allocator; 34 35 import dsymbol.builtin.names; 36 import dsymbol.builtin.symbols; 37 import dsymbol.conversion; 38 import dsymbol.modulecache; 39 import dsymbol.scope_; 40 import dsymbol.string_interning; 41 import dsymbol.symbol; 42 43 import dcd.common.constants; 44 import dcd.common.messages; 45 46 /** 47 * Handles autocompletion 48 * Params: 49 * request = the autocompletion request 50 * Returns: 51 * the autocompletion response 52 */ 53 public AutocompleteResponse complete(const AutocompleteRequest request, 54 ref ModuleCache moduleCache) 55 { 56 const(Token)[] tokenArray; 57 auto stringCache = StringCache(StringCache.defaultBucketCount); 58 auto beforeTokens = getTokensBeforeCursor(request.sourceCode, 59 request.cursorPosition, stringCache, tokenArray); 60 61 // allows to get completion on keyword, typically "is" 62 if (beforeTokens.length && isKeyword(beforeTokens[$-1].type)) 63 { 64 Token* fakeIdent = cast(Token*) (&beforeTokens[$-1]); 65 fakeIdent.text = str(fakeIdent.type); 66 fakeIdent.type = tok!"identifier"; 67 } 68 69 if (beforeTokens.length >= 2) 70 { 71 if (beforeTokens[$ - 1] == tok!"(" || beforeTokens[$ - 1] == tok!"[" 72 || beforeTokens[$ - 1] == tok!",") 73 { 74 immutable size_t end = goBackToOpenParen(beforeTokens); 75 if (end != size_t.max) 76 return parenCompletion(beforeTokens[0 .. end], tokenArray, 77 request.cursorPosition, moduleCache); 78 } 79 else 80 { 81 ImportKind kind = determineImportKind(beforeTokens); 82 if (kind == ImportKind.neither) 83 { 84 if (beforeTokens.isUdaExpression) 85 beforeTokens = beforeTokens[$-1 .. $]; 86 return dotCompletion(beforeTokens, tokenArray, request.cursorPosition, 87 moduleCache); 88 } 89 else 90 return importCompletion(beforeTokens, kind, moduleCache); 91 } 92 } 93 return dotCompletion(beforeTokens, tokenArray, request.cursorPosition, moduleCache); 94 } 95 96 /** 97 * Handles dot completion for identifiers and types. 98 * Params: 99 * beforeTokens = the tokens before the cursor 100 * tokenArray = all tokens in the file 101 * cursorPosition = the cursor position in bytes 102 * Returns: 103 * the autocompletion response 104 */ 105 AutocompleteResponse dotCompletion(T)(T beforeTokens, const(Token)[] tokenArray, 106 size_t cursorPosition, ref ModuleCache moduleCache) 107 { 108 AutocompleteResponse response; 109 110 // Partial symbol name appearing after the dot character and before the 111 // cursor. 112 string partial; 113 114 // Type of the token before the dot, or identifier if the cursor was at 115 // an identifier. 116 IdType significantTokenType; 117 118 if (beforeTokens.length >= 1 && beforeTokens[$ - 1] == tok!"identifier") 119 { 120 // Set partial to the slice of the identifier between the beginning 121 // of the identifier and the cursor. This improves the completion 122 // responses when the cursor is in the middle of an identifier instead 123 // of at the end 124 auto t = beforeTokens[$ - 1]; 125 if (cursorPosition - t.index >= 0 && cursorPosition - t.index <= t.text.length) 126 { 127 partial = t.text[0 .. cursorPosition - t.index]; 128 // issue 442 - prevent `partial` to start in the middle of a MBC 129 // since later there's a non-nothrow call to `toUpper` 130 import std.utf : validate, UTFException; 131 try validate(partial); 132 catch (UTFException) 133 { 134 import std.experimental.logger : warning; 135 warning("cursor positioned within a UTF sequence"); 136 partial = ""; 137 } 138 } 139 significantTokenType = partial.length ? tok!"identifier" : tok!""; 140 beforeTokens = beforeTokens[0 .. $ - 1]; 141 } 142 else if (beforeTokens.length >= 2 && beforeTokens[$ - 1] == tok!".") 143 significantTokenType = beforeTokens[$ - 2].type; 144 else 145 return response; 146 147 switch (significantTokenType) 148 { 149 mixin(STRING_LITERAL_CASES); 150 foreach (symbol; arraySymbols) 151 response.completions ~= makeSymbolCompletionInfo(symbol, symbol.kind); 152 response.completionType = CompletionType.identifiers; 153 break; 154 mixin(TYPE_IDENT_CASES); 155 case tok!")": 156 case tok!"]": 157 auto allocator = scoped!(ASTAllocator)(); 158 RollbackAllocator rba; 159 ScopeSymbolPair pair = generateAutocompleteTrees(tokenArray, allocator, 160 &rba, cursorPosition, moduleCache); 161 scope(exit) pair.destroy(); 162 response.setCompletions(pair.scope_, getExpression(beforeTokens), 163 cursorPosition, CompletionType.identifiers, false, partial); 164 break; 165 case tok!"(": 166 case tok!"{": 167 case tok!"[": 168 case tok!";": 169 case tok!":": 170 break; 171 default: 172 break; 173 } 174 return response; 175 } 176 177 /** 178 * Handles paren completion for function calls and some keywords 179 * Params: 180 * beforeTokens = the tokens before the cursor 181 * tokenArray = all tokens in the file 182 * cursorPosition = the cursor position in bytes 183 * Returns: 184 * the autocompletion response 185 */ 186 AutocompleteResponse parenCompletion(T)(T beforeTokens, 187 const(Token)[] tokenArray, size_t cursorPosition, ref ModuleCache moduleCache) 188 { 189 AutocompleteResponse response; 190 immutable(ConstantCompletion)[] completions; 191 switch (beforeTokens[$ - 2].type) 192 { 193 case tok!"__traits": 194 completions = traits; 195 goto fillResponse; 196 case tok!"scope": 197 completions = scopes; 198 goto fillResponse; 199 case tok!"version": 200 completions = predefinedVersions; 201 goto fillResponse; 202 case tok!"extern": 203 completions = linkages; 204 goto fillResponse; 205 case tok!"pragma": 206 completions = pragmas; 207 fillResponse: 208 response.completionType = CompletionType.identifiers; 209 foreach (completion; completions) 210 { 211 response.completions ~= AutocompleteResponse.Completion( 212 completion.identifier, 213 CompletionKind.keyword, 214 null, null, 0, // definition, symbol path+location 215 completion.ddoc 216 ); 217 } 218 break; 219 case tok!"characterLiteral": 220 case tok!"doubleLiteral": 221 case tok!"floatLiteral": 222 case tok!"identifier": 223 case tok!"idoubleLiteral": 224 case tok!"ifloatLiteral": 225 case tok!"intLiteral": 226 case tok!"irealLiteral": 227 case tok!"longLiteral": 228 case tok!"realLiteral": 229 case tok!"uintLiteral": 230 case tok!"ulongLiteral": 231 case tok!"this": 232 case tok!"super": 233 case tok!")": 234 case tok!"]": 235 mixin(STRING_LITERAL_CASES); 236 auto allocator = scoped!(ASTAllocator)(); 237 RollbackAllocator rba; 238 ScopeSymbolPair pair = generateAutocompleteTrees(tokenArray, allocator, 239 &rba, cursorPosition, moduleCache); 240 scope(exit) pair.destroy(); 241 auto expression = getExpression(beforeTokens[0 .. $ - 1]); 242 response.setCompletions(pair.scope_, expression, 243 cursorPosition, CompletionType.calltips, beforeTokens[$ - 1] == tok!"["); 244 break; 245 default: 246 break; 247 } 248 return response; 249 } 250 251 /** 252 * Provides autocomplete for selective imports, e.g.: 253 * --- 254 * import std.algorithm: balancedParens; 255 * --- 256 */ 257 AutocompleteResponse importCompletion(T)(T beforeTokens, ImportKind kind, 258 ref ModuleCache moduleCache) 259 in 260 { 261 assert (beforeTokens.length >= 2); 262 } 263 body 264 { 265 AutocompleteResponse response; 266 if (beforeTokens.length <= 2) 267 return response; 268 269 size_t i = beforeTokens.length - 1; 270 271 if (kind == ImportKind.normal) 272 { 273 274 while (beforeTokens[i].type != tok!"," && beforeTokens[i].type != tok!"import" 275 && beforeTokens[i].type != tok!"=" ) 276 i--; 277 setImportCompletions(beforeTokens[i .. $], response, moduleCache); 278 return response; 279 } 280 281 loop: while (true) switch (beforeTokens[i].type) 282 { 283 case tok!"identifier": 284 case tok!"=": 285 case tok!",": 286 case tok!".": 287 i--; 288 break; 289 case tok!":": 290 i--; 291 while (beforeTokens[i].type == tok!"identifier" || beforeTokens[i].type == tok!".") 292 i--; 293 break loop; 294 default: 295 break loop; 296 } 297 298 size_t j = i; 299 loop2: while (j <= beforeTokens.length) switch (beforeTokens[j].type) 300 { 301 case tok!":": break loop2; 302 default: j++; break; 303 } 304 305 if (i >= j) 306 { 307 warning("Malformed import statement"); 308 return response; 309 } 310 311 immutable string path = beforeTokens[i + 1 .. j] 312 .filter!(token => token.type == tok!"identifier") 313 .map!(token => cast() token.text) 314 .joiner(dirSeparator) 315 .text(); 316 317 string resolvedLocation = moduleCache.resolveImportLocation(path); 318 if (resolvedLocation is null) 319 { 320 warning("Could not resolve location of ", path); 321 return response; 322 } 323 auto symbols = moduleCache.getModuleSymbol(internString(resolvedLocation)); 324 325 import containers.hashset : HashSet; 326 HashSet!string h; 327 328 void addSymbolToResponses(const(DSymbol)* sy) 329 { 330 auto a = DSymbol(sy.name); 331 if (!builtinSymbols.contains(&a) && sy.name !is null && !h.contains(sy.name) 332 && !sy.skipOver && sy.name != CONSTRUCTOR_SYMBOL_NAME 333 && isPublicCompletionKind(sy.kind)) 334 { 335 response.completions ~= makeSymbolCompletionInfo(sy, sy.kind); 336 h.insert(sy.name); 337 } 338 } 339 340 foreach (s; symbols.opSlice().filter!(a => !a.skipOver)) 341 { 342 if (s.kind == CompletionKind.importSymbol && s.type !is null) 343 foreach (sy; s.type.opSlice().filter!(a => !a.skipOver)) 344 addSymbolToResponses(sy); 345 else 346 addSymbolToResponses(s); 347 } 348 response.completionType = CompletionType.identifiers; 349 return response; 350 } 351 352 /** 353 * Populates the response with completion information for an import statement 354 * Params: 355 * tokens = the tokens after the "import" keyword and before the cursor 356 * response = the response that should be populated 357 */ 358 void setImportCompletions(T)(T tokens, ref AutocompleteResponse response, 359 ref ModuleCache cache) 360 { 361 response.completionType = CompletionType.identifiers; 362 string partial = null; 363 if (tokens[$ - 1].type == tok!"identifier") 364 { 365 partial = tokens[$ - 1].text; 366 tokens = tokens[0 .. $ - 1]; 367 } 368 auto moduleParts = tokens.filter!(a => a.type == tok!"identifier").map!("a.text").array(); 369 string path = buildPath(moduleParts); 370 371 bool found = false; 372 373 foreach (importPath; cache.getImportPaths()) 374 { 375 if (importPath.isFile) 376 { 377 if (!exists(importPath)) 378 continue; 379 380 found = true; 381 382 auto n = importPath.baseName(".d").baseName(".di"); 383 if (isFile(importPath) && (importPath.endsWith(".d") || importPath.endsWith(".di")) 384 && (partial is null || n.startsWith(partial))) 385 response.completions ~= AutocompleteResponse.Completion(n, CompletionKind.moduleName, null, importPath, 0); 386 } 387 else 388 { 389 string p = buildPath(importPath, path); 390 if (!exists(p)) 391 continue; 392 393 found = true; 394 395 try foreach (string name; dirEntries(p, SpanMode.shallow)) 396 { 397 import std.path: baseName; 398 if (name.baseName.startsWith(".#")) 399 continue; 400 401 auto n = name.baseName(".d").baseName(".di"); 402 if (isFile(name) && (name.endsWith(".d") || name.endsWith(".di")) 403 && (partial is null || n.startsWith(partial))) 404 response.completions ~= AutocompleteResponse.Completion(n, CompletionKind.moduleName, null, name, 0); 405 else if (isDir(name)) 406 { 407 if (n[0] != '.' && (partial is null || n.startsWith(partial))) 408 { 409 immutable packageDPath = buildPath(name, "package.d"); 410 immutable packageDIPath = buildPath(name, "package.di"); 411 immutable packageD = exists(packageDPath); 412 immutable packageDI = exists(packageDIPath); 413 immutable kind = packageD || packageDI ? CompletionKind.moduleName : CompletionKind.packageName; 414 immutable file = packageD ? packageDPath : packageDI ? packageDIPath : name; 415 response.completions ~= AutocompleteResponse.Completion(n, kind, null, file, 0); 416 } 417 } 418 } 419 catch(FileException) 420 { 421 warning("Cannot access import path: ", importPath); 422 } 423 } 424 } 425 if (!found) 426 warning("Could not find ", moduleParts); 427 } 428 429 /** 430 * 431 */ 432 void setCompletions(T)(ref AutocompleteResponse response, 433 Scope* completionScope, T tokens, size_t cursorPosition, 434 CompletionType completionType, bool isBracket = false, string partial = null) 435 { 436 static void addSymToResponse(const(DSymbol)* s, ref AutocompleteResponse r, string p, 437 size_t[] circularGuard = []) 438 { 439 if (circularGuard.canFind(cast(size_t) s)) 440 return; 441 foreach (sym; s.opSlice()) 442 { 443 if (sym.name !is null && sym.name.length > 0 && isPublicCompletionKind(sym.kind) 444 && (p is null ? true : toUpper(sym.name.data).startsWith(toUpper(p))) 445 && !r.completions.canFind!(a => a.identifier == sym.name) 446 && sym.name[0] != '*') 447 { 448 r.completions ~= makeSymbolCompletionInfo(sym, sym.kind); 449 } 450 if (sym.kind == CompletionKind.importSymbol && !sym.skipOver && sym.type !is null) 451 addSymToResponse(sym.type, r, p, circularGuard ~ (cast(size_t) s)); 452 } 453 } 454 455 // Handle the simple case where we get all symbols in scope and filter it 456 // based on the currently entered text. 457 if (partial !is null && tokens.length == 0) 458 { 459 auto currentSymbols = completionScope.getSymbolsInCursorScope(cursorPosition); 460 foreach (s; currentSymbols.filter!(a => isPublicCompletionKind(a.kind) 461 && toUpper(a.name.data).startsWith(toUpper(partial)))) 462 { 463 response.completions ~= makeSymbolCompletionInfo(s, s.kind); 464 } 465 response.completionType = CompletionType.identifiers; 466 return; 467 } 468 469 if (tokens.length == 0) 470 return; 471 472 DSymbol*[] symbols = getSymbolsByTokenChain(completionScope, tokens, 473 cursorPosition, completionType); 474 475 if (symbols.length == 0) 476 return; 477 478 if (completionType == CompletionType.identifiers) 479 { 480 while (symbols[0].qualifier == SymbolQualifier.func 481 || symbols[0].kind == CompletionKind.functionName 482 || symbols[0].kind == CompletionKind.importSymbol 483 || symbols[0].kind == CompletionKind.aliasName) 484 { 485 symbols = symbols[0].type is null || symbols[0].type is symbols[0] ? [] 486 : [symbols[0].type]; 487 if (symbols.length == 0) 488 return; 489 } 490 addSymToResponse(symbols[0], response, partial); 491 response.completionType = CompletionType.identifiers; 492 } 493 else if (completionType == CompletionType.calltips) 494 { 495 //trace("Showing call tips for ", symbols[0].name, " of kind ", symbols[0].kind); 496 if (symbols[0].kind != CompletionKind.functionName 497 && symbols[0].callTip is null) 498 { 499 if (symbols[0].kind == CompletionKind.aliasName) 500 { 501 if (symbols[0].type is null || symbols[0].type is symbols[0]) 502 return; 503 symbols = [symbols[0].type]; 504 } 505 if (symbols[0].kind == CompletionKind.variableName) 506 { 507 auto dumb = symbols[0].type; 508 if (dumb !is null) 509 { 510 if (dumb.kind == CompletionKind.functionName) 511 { 512 symbols = [dumb]; 513 goto setCallTips; 514 } 515 if (isBracket) 516 { 517 auto index = dumb.getPartsByName(internString("opIndex")); 518 if (index.length > 0) 519 { 520 symbols = index; 521 goto setCallTips; 522 } 523 } 524 auto call = dumb.getPartsByName(internString("opCall")); 525 if (call.length > 0) 526 { 527 symbols = call; 528 goto setCallTips; 529 } 530 } 531 } 532 if (symbols[0].kind == CompletionKind.structName 533 || symbols[0].kind == CompletionKind.className) 534 { 535 auto constructor = symbols[0].getPartsByName(CONSTRUCTOR_SYMBOL_NAME); 536 if (constructor.length == 0) 537 { 538 // Build a call tip out of the struct fields 539 if (symbols[0].kind == CompletionKind.structName) 540 { 541 response.completionType = CompletionType.calltips; 542 response.completions = [generateStructConstructorCalltip(symbols[0])]; 543 return; 544 } 545 } 546 else 547 { 548 symbols = constructor; 549 goto setCallTips; 550 } 551 } 552 } 553 setCallTips: 554 response.completionType = CompletionType.calltips; 555 foreach (symbol; symbols) 556 { 557 if (symbol.kind != CompletionKind.aliasName && symbol.callTip !is null) 558 { 559 auto completion = makeSymbolCompletionInfo(symbol, char.init); 560 // TODO: put return type 561 response.completions ~= completion; 562 } 563 } 564 } 565 } 566 567 AutocompleteResponse.Completion generateStructConstructorCalltip(const DSymbol* symbol) 568 in 569 { 570 assert(symbol.kind == CompletionKind.structName); 571 } 572 body 573 { 574 string generatedStructConstructorCalltip = "this("; 575 const(DSymbol)*[] fields = symbol.opSlice().filter!( 576 a => a.kind == CompletionKind.variableName).map!(a => cast(const(DSymbol)*) a).array(); 577 fields.sort!((a, b) => a.location < b.location); 578 foreach (i, field; fields) 579 { 580 if (field.kind != CompletionKind.variableName) 581 continue; 582 i++; 583 if (field.type !is null) 584 { 585 generatedStructConstructorCalltip ~= field.type.name; 586 generatedStructConstructorCalltip ~= " "; 587 } 588 generatedStructConstructorCalltip ~= field.name; 589 if (i < fields.length) 590 generatedStructConstructorCalltip ~= ", "; 591 } 592 generatedStructConstructorCalltip ~= ")"; 593 auto completion = makeSymbolCompletionInfo(symbol, char.init); 594 completion.identifier = "this"; 595 completion.definition = generatedStructConstructorCalltip; 596 return completion; 597 }