• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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