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