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