1namespace ts.projectSystem { 2 describe("unittests:: tsserver:: completions", () => { 3 it("works", () => { 4 const aTs: File = { 5 path: "/a.ts", 6 content: "export const foo = 0;", 7 }; 8 const bTs: File = { 9 path: "/b.ts", 10 content: "foo", 11 }; 12 const tsconfig: File = { 13 path: "/tsconfig.json", 14 content: "{}", 15 }; 16 17 const session = createSession(createServerHost([aTs, bTs, tsconfig])); 18 openFilesForSession([aTs, bTs], session); 19 20 const requestLocation: protocol.FileLocationRequestArgs = { 21 file: bTs.path, 22 line: 1, 23 offset: 3, 24 }; 25 26 const response = executeSessionRequest<protocol.CompletionsRequest, protocol.CompletionInfoResponse>(session, protocol.CommandTypes.CompletionInfo, { 27 ...requestLocation, 28 includeExternalModuleExports: true, 29 prefix: "foo", 30 }); 31 const entry: protocol.CompletionEntry = { 32 hasAction: true, 33 insertText: undefined, 34 isRecommended: undefined, 35 kind: ScriptElementKind.constElement, 36 kindModifiers: ScriptElementKindModifier.exportedModifier, 37 name: "foo", 38 replacementSpan: undefined, 39 isPackageJsonImport: undefined, 40 isImportStatementCompletion: undefined, 41 sortText: Completions.SortText.AutoImportSuggestions, 42 source: "/a", 43 sourceDisplay: undefined, 44 isSnippet: undefined, 45 data: { exportName: "foo", fileName: "/a.ts", ambientModuleName: undefined, isPackageJsonImport: undefined }, 46 labelDetails: undefined, 47 jsDoc: undefined, 48 displayParts: [ 49 { 50 text: "const", 51 kind: "keyword" 52 },{ 53 text: " ", 54 kind: "space" 55 },{ 56 text: "foo", 57 kind: "localName" 58 },{ 59 text: ":", 60 kind: "punctuation" 61 },{ 62 text: " ", 63 kind: "space" 64 },{ 65 text: "0", 66 kind: "stringLiteral" 67 } 68 ] 69 }; 70 71 // `data.exportMapKey` contains a SymbolId so should not be mocked up with an expected value here. 72 // Just assert that it's a string and then delete it so we can compare everything else with `deepEqual`. 73 const exportMapKey = (response?.entries[0].data as any)?.exportMapKey; 74 assert.isString(exportMapKey); 75 delete (response?.entries[0].data as any).exportMapKey; 76 assert.deepEqual<protocol.CompletionInfo | undefined>(response, { 77 flags: CompletionInfoFlags.MayIncludeAutoImports, 78 isGlobalCompletion: true, 79 isIncomplete: undefined, 80 isMemberCompletion: false, 81 isNewIdentifierLocation: false, 82 optionalReplacementSpan: { start: { line: 1, offset: 1 }, end: { line: 1, offset: 4 } }, 83 entries: [entry], 84 }); 85 86 const detailsRequestArgs: protocol.CompletionDetailsRequestArgs = { 87 ...requestLocation, 88 entryNames: [{ name: "foo", source: "/a", data: { exportName: "foo", fileName: "/a.ts", exportMapKey } }], 89 }; 90 91 const detailsResponse = executeSessionRequest<protocol.CompletionDetailsRequest, protocol.CompletionDetailsResponse>(session, protocol.CommandTypes.CompletionDetails, detailsRequestArgs); 92 const detailsCommon: protocol.CompletionEntryDetails & CompletionEntryDetails = { 93 displayParts: [ 94 keywordPart(SyntaxKind.ConstKeyword), 95 spacePart(), 96 displayPart("foo", SymbolDisplayPartKind.localName), 97 punctuationPart(SyntaxKind.ColonToken), 98 spacePart(), 99 displayPart("0", SymbolDisplayPartKind.stringLiteral), 100 ], 101 documentation: emptyArray, 102 kind: ScriptElementKind.constElement, 103 kindModifiers: ScriptElementKindModifier.exportedModifier, 104 name: "foo", 105 source: [{ text: "./a", kind: "text" }], 106 sourceDisplay: [{ text: "./a", kind: "text" }], 107 }; 108 assert.deepEqual<readonly protocol.CompletionEntryDetails[] | undefined>(detailsResponse, [ 109 { 110 codeActions: [ 111 { 112 description: `Add import from "./a"`, 113 changes: [ 114 { 115 fileName: "/b.ts", 116 textChanges: [ 117 { 118 start: { line: 1, offset: 1 }, 119 end: { line: 1, offset: 1 }, 120 newText: 'import { foo } from "./a";\n\n', 121 }, 122 ], 123 }, 124 ], 125 commands: undefined, 126 }, 127 ], 128 tags: [], 129 ...detailsCommon, 130 }, 131 ]); 132 133 interface CompletionDetailsFullRequest extends protocol.FileLocationRequest { 134 readonly command: protocol.CommandTypes.CompletionDetailsFull; 135 readonly arguments: protocol.CompletionDetailsRequestArgs; 136 } 137 interface CompletionDetailsFullResponse extends protocol.Response { 138 readonly body?: readonly CompletionEntryDetails[]; 139 } 140 const detailsFullResponse = executeSessionRequest<CompletionDetailsFullRequest, CompletionDetailsFullResponse>(session, protocol.CommandTypes.CompletionDetailsFull, detailsRequestArgs); 141 assert.deepEqual<readonly CompletionEntryDetails[] | undefined>(detailsFullResponse, [ 142 { 143 codeActions: [ 144 { 145 description: `Add import from "./a"`, 146 changes: [ 147 { 148 fileName: "/b.ts", 149 textChanges: [createTextChange(createTextSpan(0, 0), 'import { foo } from "./a";\n\n')], 150 }, 151 ], 152 commands: undefined, 153 } 154 ], 155 tags: [], 156 ...detailsCommon, 157 } 158 ]); 159 }); 160 161 it("works add jsDoc info at interface getCompletionsAtPosition", () => { 162 const aTs: File = { 163 path: "/a.ts", 164 content: `export class Test { 165/** 166 * @devices tv 167 */ 168public test(): void { 169 170} 171}` 172 }; 173 const bTs: File = { 174 path: "/b.ts", 175 content: `import { Test } from "./a"; 176const test = new Test(); 177test.`, 178 }; 179 const tsconfig: File = { 180 path: "/tsconfig.json", 181 content: "{}", 182 }; 183 184 const session = createSession(createServerHost([aTs, bTs, tsconfig])); 185 openFilesForSession([aTs, bTs], session); 186 187 const requestLocation: protocol.FileLocationRequestArgs = { 188 file: bTs.path, 189 line: 3, 190 offset: 6 191 }; 192 193 const response = executeSessionRequest<protocol.CompletionsRequest, protocol.CompletionInfoResponse>(session, protocol.CommandTypes.CompletionInfo, { 194 ...requestLocation, 195 includeExternalModuleExports: true, 196 prefix: ".", 197 }); 198 const entry: protocol.CompletionEntry = { 199 name: "test", 200 kind: ScriptElementKind.memberFunctionElement, 201 kindModifiers: ScriptElementKindModifier.publicMemberModifier, 202 sortText: "11", 203 insertText: undefined, 204 replacementSpan: undefined, 205 isSnippet: undefined, 206 hasAction: undefined, 207 source: undefined, 208 sourceDisplay: undefined, 209 labelDetails: undefined, 210 isRecommended: undefined, 211 isPackageJsonImport: undefined, 212 isImportStatementCompletion: undefined, 213 jsDoc: [{ 214 name: "devices", 215 text: [ 216 { 217 text: "tv", 218 kind: "text" 219 } 220 ], 221 }], 222 displayParts: [ 223 { 224 text: "(", 225 kind: "punctuation" 226 },{ 227 text: "method", 228 kind: "text" 229 },{ 230 text: ")", 231 kind: "punctuation" 232 },{ 233 text: " ", 234 kind: "space" 235 },{ 236 text: "Test", 237 kind: "className" 238 },{ 239 text: ".", 240 kind: "punctuation" 241 },{ 242 text: "test", 243 kind: "methodName" 244 },{ 245 text: "(", 246 kind: "punctuation" 247 },{ 248 text: ")", 249 kind: "punctuation" 250 },{ 251 text: ":", 252 kind: "punctuation" 253 },{ 254 text: " ", 255 kind: "space" 256 },{ 257 text: "void", 258 kind: "keyword" 259 } 260 ], 261 data: undefined, 262 }; 263 assert.deepEqual<protocol.CompletionInfo | undefined>(response, { 264 flags: CompletionInfoFlags.None, 265 isGlobalCompletion: false, 266 isIncomplete: undefined, 267 isMemberCompletion: true, 268 isNewIdentifierLocation: false, 269 optionalReplacementSpan: undefined, 270 entries: [entry], 271 }); 272 }); 273 274 it("works when files are included from two different drives of windows", () => { 275 const projectRoot = "e:/myproject"; 276 const appPackage: File = { 277 path: `${projectRoot}/package.json`, 278 content: JSON.stringify({ 279 name: "test", 280 version: "0.1.0", 281 dependencies: { 282 "react": "^16.12.0", 283 "react-router-dom": "^5.1.2", 284 } 285 }) 286 }; 287 const appFile: File = { 288 path: `${projectRoot}/src/app.js`, 289 content: `import React from 'react'; 290import { 291 BrowserRouter as Router, 292} from "react-router-dom"; 293` 294 }; 295 const localNodeModules = `${projectRoot}/node_modules`; 296 const localAtTypes = `${localNodeModules}/@types`; 297 const localReactPackage: File = { 298 path: `${localAtTypes}/react/package.json`, 299 content: JSON.stringify({ 300 name: "@types/react", 301 version: "16.9.14", 302 }) 303 }; 304 const localReact: File = { 305 path: `${localAtTypes}/react/index.d.ts`, 306 content: `import * as PropTypes from 'prop-types'; 307` 308 }; 309 const localReactRouterDomPackage: File = { 310 path: `${localNodeModules}/react-router-dom/package.json`, 311 content: JSON.stringify({ 312 name: "react-router-dom", 313 version: "5.1.2", 314 }) 315 }; 316 const localReactRouterDom: File = { 317 path: `${localNodeModules}/react-router-dom/index.js`, 318 content: `export function foo() {}` 319 }; 320 const localPropTypesPackage: File = { 321 path: `${localAtTypes}/prop-types/package.json`, 322 content: JSON.stringify({ 323 name: "@types/prop-types", 324 version: "15.7.3", 325 }) 326 }; 327 const localPropTypes: File = { 328 path: `${localAtTypes}/prop-types/index.d.ts`, 329 content: `export type ReactComponentLike = 330 | string 331 | ((props: any, context?: any) => any) 332 | (new (props: any, context?: any) => any); 333` 334 }; 335 336 const globalCacheLocation = `c:/typescript`; 337 const globalAtTypes = `${globalCacheLocation}/node_modules/@types`; 338 const globalReactRouterDomPackage: File = { 339 path: `${globalAtTypes}/react-router-dom/package.json`, 340 content: JSON.stringify({ 341 name: "@types/react-router-dom", 342 version: "5.1.2", 343 }) 344 }; 345 const globalReactRouterDom: File = { 346 path: `${globalAtTypes}/react-router-dom/index.d.ts`, 347 content: `import * as React from 'react'; 348export interface BrowserRouterProps { 349 basename?: string; 350 getUserConfirmation?: ((message: string, callback: (ok: boolean) => void) => void); 351 forceRefresh?: boolean; 352 keyLength?: number; 353}` 354 }; 355 const globalReactPackage: File = { 356 path: `${globalAtTypes}/react/package.json`, 357 content: localReactPackage.content 358 }; 359 const globalReact: File = { 360 path: `${globalAtTypes}/react/index.d.ts`, 361 content: localReact.content 362 }; 363 364 const filesInProject = [ 365 appFile, 366 localReact, 367 localPropTypes, 368 globalReactRouterDom, 369 globalReact, 370 ]; 371 const files = [ 372 ...filesInProject, 373 appPackage, libFile, 374 localReactPackage, 375 localReactRouterDomPackage, localReactRouterDom, 376 localPropTypesPackage, 377 globalReactRouterDomPackage, 378 globalReactPackage, 379 ]; 380 381 const host = createServerHost(files, { windowsStyleRoot: "c:/" }); 382 const session = createSession(host, { 383 typingsInstaller: new TestTypingsInstaller(globalCacheLocation, /*throttleLimit*/ 5, host), 384 }); 385 const service = session.getProjectService(); 386 openFilesForSession([appFile], session); 387 checkNumberOfProjects(service, { inferredProjects: 1 }); 388 const windowsStyleLibFilePath = "c:/" + libFile.path.substring(1); 389 checkProjectActualFiles(service.inferredProjects[0], filesInProject.map(f => f.path).concat(windowsStyleLibFilePath)); 390 session.executeCommandSeq<protocol.CompletionsRequest>({ 391 command: protocol.CommandTypes.CompletionInfo, 392 arguments: { 393 file: appFile.path, 394 line: 5, 395 offset: 1, 396 includeExternalModuleExports: true, 397 includeInsertTextCompletions: true 398 } 399 }); 400 }); 401 }); 402} 403