Skip to content

Commit b08f5e7

Browse files
authored
Merge pull request #1290 from vuejs/path-completion
Path completion. Fix #822
2 parents bcce40d + 835587e commit b08f5e7

File tree

10 files changed

+224
-104
lines changed

10 files changed

+224
-104
lines changed

server/src/modes/script/javascript.ts

+33-3
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ import { IServiceHost } from '../../services/typescriptService/serviceHost';
4444

4545
// Todo: After upgrading to LS server 4.0, use CompletionContext for filtering trigger chars
4646
// https://microsoft.github.io/language-server-protocol/specification#completion-request-leftwards_arrow_with_hook
47-
const NON_SCRIPT_TRIGGERS = ['<', '/', '*', ':'];
47+
const NON_SCRIPT_TRIGGERS = ['<', '*', ':'];
4848

4949
export async function getJavascriptMode(
5050
serviceHost: IServiceHost,
@@ -156,10 +156,12 @@ export async function getJavascriptMode(
156156
isIncomplete: false,
157157
items: entries.map((entry, index) => {
158158
const range = entry.replacementSpan && convertRange(scriptDoc, entry.replacementSpan);
159+
const { label, detail } = calculateLabelAndDetailTextForPathImport(entry);
159160
return {
160161
uri: doc.uri,
161162
position,
162-
label: entry.name,
163+
label,
164+
detail,
163165
sortText: entry.sortText + index,
164166
kind: convertKind(entry.kind),
165167
textEdit: range && TextEdit.replace(range, entry.name),
@@ -173,6 +175,30 @@ export async function getJavascriptMode(
173175
};
174176
})
175177
};
178+
179+
function calculateLabelAndDetailTextForPathImport(entry: ts.CompletionEntry) {
180+
// Is import path completion
181+
if (entry.kind === ts.ScriptElementKind.scriptElement) {
182+
if (entry.kindModifiers) {
183+
return {
184+
label: entry.name,
185+
detail: entry.name + entry.kindModifiers
186+
};
187+
} else {
188+
if (entry.name.endsWith('.vue')) {
189+
return {
190+
label: entry.name.slice(0, -'.vue'.length),
191+
detail: entry.name
192+
};
193+
}
194+
}
195+
}
196+
197+
return {
198+
label: entry.name,
199+
detail: undefined
200+
};
201+
}
176202
},
177203
doResolve(doc: TextDocument, item: CompletionItem): CompletionItem {
178204
const { service } = updateCurrentVueTextDocument(doc);
@@ -193,7 +219,7 @@ export async function getJavascriptMode(
193219
includeCompletionsWithInsertText: true
194220
}
195221
);
196-
if (details) {
222+
if (details && item.kind !== CompletionItemKind.File && item.kind !== CompletionItemKind.Folder) {
197223
item.detail = tsModule.displayPartsToString(details.displayParts);
198224
const documentation: MarkupContent = {
199225
kind: 'markdown',
@@ -641,6 +667,10 @@ function convertKind(kind: ts.ScriptElementKind): CompletionItemKind {
641667
return CompletionItemKind.Interface;
642668
case 'warning':
643669
return CompletionItemKind.File;
670+
case 'script':
671+
return CompletionItemKind.File;
672+
case 'directory':
673+
return CompletionItemKind.Folder;
644674
}
645675

646676
return CompletionItemKind.Property;

server/src/services/typescriptService/serviceHost.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,16 @@ export function getServiceHost(
231231
directoryExists: vueSys.directoryExists,
232232
fileExists: vueSys.fileExists,
233233
readFile: vueSys.readFile,
234-
readDirectory: vueSys.readDirectory,
234+
readDirectory(
235+
path: string,
236+
extensions?: ReadonlyArray<string>,
237+
exclude?: ReadonlyArray<string>,
238+
include?: ReadonlyArray<string>,
239+
depth?: number
240+
): string[] {
241+
const allExtensions = extensions ? extensions.concat(['.vue']) : extensions;
242+
return vueSys.readDirectory(path, allExtensions, exclude, include, depth);
243+
},
235244

236245
resolveModuleNames(moduleNames: string[], containingFile: string): ts.ResolvedModule[] {
237246
// in the normal case, delegate to ts.resolveModuleName

test/interpolation/completion/basic.test.ts

+23-42
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { activateLS, showFile, sleep, FILE_LOAD_SLEEP_TIME } from '../../lsp/helper';
22
import { position, getDocUri } from '../util';
33
import { testCompletion, testNoSuchCompletion } from './helper';
4-
import { CompletionItem, CompletionItemKind } from 'vscode-languageserver-types';
4+
import { CompletionItem, CompletionItemKind, MarkdownString } from 'vscode';
55

66
describe('Should autocomplete interpolation for <template>', () => {
77
const templateDocUri = getDocUri('completion/Basic.vue');
@@ -14,60 +14,42 @@ describe('Should autocomplete interpolation for <template>', () => {
1414
await sleep(FILE_LOAD_SLEEP_TIME);
1515
});
1616

17-
function wrapWithJSCodeRegion(src: string) {
18-
return '\n```js\n' + src + '\n```\n';
19-
}
20-
2117
const defaultList: CompletionItem[] = [
2218
{
2319
label: 'foo',
24-
documentation: {
25-
kind: 'markdown',
26-
value:
27-
'My foo' +
28-
wrapWithJSCodeRegion(
29-
`foo: {
20+
documentation: new MarkdownString('My foo').appendCodeblock(
21+
`foo: {
3022
type: Boolean,
3123
default: false
32-
}`
33-
)
34-
},
24+
}`,
25+
'js'
26+
),
3527
kind: CompletionItemKind.Property
3628
},
3729
{
3830
label: 'msg',
39-
documentation: {
40-
kind: 'markdown',
41-
value: 'My msg' + wrapWithJSCodeRegion(`msg: 'Vetur means "Winter" in icelandic.'`)
42-
},
31+
documentation: new MarkdownString('My msg').appendCodeblock(`msg: 'Vetur means "Winter" in icelandic.'`, 'js'),
4332
kind: CompletionItemKind.Property
4433
},
4534
{
4635
label: 'count',
47-
documentation: {
48-
kind: 'markdown',
49-
value:
50-
'My count' +
51-
wrapWithJSCodeRegion(
52-
`count () {
36+
documentation: new MarkdownString('My count').appendCodeblock(
37+
`count () {
5338
return this.$store.state.count
54-
}`
55-
)
56-
},
39+
}`,
40+
'js'
41+
),
5742
kind: CompletionItemKind.Property
5843
},
5944
{
6045
label: 'hello',
61-
documentation: {
62-
kind: 'markdown',
63-
value:
64-
'My greeting' +
65-
wrapWithJSCodeRegion(
66-
`hello () {
46+
documentation: new MarkdownString('My greeting').appendCodeblock(
47+
`hello () {
6748
console.log(this.msg)
68-
}`
69-
)
70-
},
49+
}`,
50+
'js'
51+
),
52+
7153
kind: CompletionItemKind.Method
7254
}
7355
];
@@ -90,14 +72,13 @@ describe('Should autocomplete interpolation for <template>', () => {
9072
await testCompletion(parentTemplateDocUri, position(2, 12), [
9173
{
9274
label: 'foo',
93-
documentation:
94-
'My foo' +
95-
wrapWithJSCodeRegion(
96-
`foo: {
75+
documentation: new MarkdownString('My foo').appendCodeblock(
76+
`foo: {
9777
type: Boolean,
9878
default: false
99-
}`
100-
)
79+
}`,
80+
'js'
81+
)
10182
}
10283
]);
10384
});

test/interpolation/completion/helper.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as vscode from 'vscode';
22
import * as assert from 'assert';
33
import { showFile } from '../helper';
4-
import { CompletionItem, MarkupContent } from 'vscode-languageserver-types';
4+
import { CompletionItem, MarkdownString } from 'vscode';
55

66
export interface ExpectedCompletionItem extends CompletionItem {
77
documentationStart?: string;
@@ -41,10 +41,10 @@ export async function testCompletion(
4141
if (typeof match.documentation === 'string') {
4242
assert.equal(normalizeNewline(match.documentation), normalizeNewline(ei.documentation as string));
4343
} else {
44-
if (ei.documentation && (ei.documentation as MarkupContent).value && match.documentation) {
44+
if (ei.documentation && (ei.documentation as MarkdownString).value && match.documentation) {
4545
assert.equal(
4646
normalizeNewline((match.documentation as vscode.MarkdownString).value),
47-
normalizeNewline((ei.documentation as MarkupContent).value)
47+
normalizeNewline((ei.documentation as MarkdownString).value)
4848
);
4949
}
5050
}

test/lsp-ts-28/completion/helper.ts

+45-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as vscode from 'vscode';
22
import * as assert from 'assert';
33
import { showFile } from '../helper';
4-
import { CompletionItem, MarkupContent } from 'vscode-languageclient';
4+
import { CompletionItem, MarkdownString } from 'vscode';
55

66
export interface ExpectedCompletionItem extends CompletionItem {
77
documentationStart?: string;
@@ -34,17 +34,22 @@ export async function testCompletion(
3434
return;
3535
}
3636

37-
assert.ok(match.label, ei.label);
38-
assert.ok(match.kind, ei.kind as any);
37+
assert.equal(match.label, ei.label);
38+
if (ei.kind) {
39+
assert.equal(match.kind, ei.kind);
40+
}
41+
if (ei.detail) {
42+
assert.equal(match.detail, ei.detail);
43+
}
3944

4045
if (ei.documentation) {
4146
if (typeof match.documentation === 'string') {
4247
assert.equal(match.documentation, ei.documentation);
4348
} else {
44-
if (ei.documentation && (ei.documentation as MarkupContent).value && match.documentation) {
49+
if (ei.documentation && (ei.documentation as MarkdownString).value && match.documentation) {
4550
assert.equal(
4651
(match.documentation as vscode.MarkdownString).value,
47-
(ei.documentation as MarkupContent).value
52+
(ei.documentation as MarkdownString).value
4853
);
4954
}
5055
}
@@ -60,3 +65,38 @@ export async function testCompletion(
6065
}
6166
});
6267
}
68+
69+
export async function testNoSuchCompletion(
70+
docUri: vscode.Uri,
71+
position: vscode.Position,
72+
notExpectedItems: (string | ExpectedCompletionItem)[]
73+
) {
74+
await showFile(docUri);
75+
76+
const result = (await vscode.commands.executeCommand(
77+
'vscode.executeCompletionItemProvider',
78+
docUri,
79+
position
80+
)) as vscode.CompletionList;
81+
82+
notExpectedItems.forEach(ei => {
83+
if (typeof ei === 'string') {
84+
assert.ok(
85+
!result.items.some(i => {
86+
return i.label === ei;
87+
})
88+
);
89+
} else {
90+
const match = result.items.find(i => {
91+
for (const x in ei) {
92+
if (ei[x] !== i[x]) {
93+
return false;
94+
}
95+
}
96+
return true;
97+
});
98+
99+
assert.ok(!match, `Shouldn't find perfect match for ${JSON.stringify(ei, null, 2)}`);
100+
}
101+
});
102+
}

0 commit comments

Comments
 (0)