Thanks to visit codestin.com
Credit goes to github.com

Skip to content

fix: snippet completions returned to clients that don't support them #556

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
},
"devDependencies": {
"@types/chai": "^4.3.1",
"@types/deepmerge": "^2.2.0",
"@types/fs-extra": "^9.0.13",
"@types/mocha": "^9.1.1",
"@types/node": "^16.11.47",
Expand All @@ -64,6 +65,7 @@
"chai": "^4.3.6",
"concurrently": "^7.3.0",
"cross-env": "^7.0.3",
"deepmerge": "^4.2.2",
"eslint": "^8.21.0",
"husky": "4.x",
"mocha": "^10.0.0",
Expand Down
29 changes: 14 additions & 15 deletions src/completion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,15 @@ import { CompletionOptions, DisplayPartKind, SupportedFeatures } from './ts-prot
import SnippetString from './utils/SnippetString.js';
import * as typeConverters from './utils/typeConverters.js';

interface TSCompletionItem extends lsp.CompletionItem {
data: tsp.CompletionDetailsRequestArgs;
}

interface ParameterListParts {
readonly parts: ReadonlyArray<tsp.SymbolDisplayPart>;
readonly hasOptionalParameters: boolean;
}

export function asCompletionItem(entry: tsp.CompletionEntry, file: string, position: lsp.Position, document: LspDocument, features: SupportedFeatures): TSCompletionItem {
const item: TSCompletionItem = {
export function asCompletionItem(entry: tsp.CompletionEntry, file: string, position: lsp.Position, document: LspDocument, features: SupportedFeatures): lsp.CompletionItem | null {
const item: lsp.CompletionItem = {
label: entry.name,
...features.labelDetails ? { labelDetails: entry.labelDetails } : {},
...features.completionLabelDetails ? { labelDetails: entry.labelDetails } : {},
kind: asCompletionItemKind(entry.kind),
sortText: entry.sortText,
commitCharacters: asCommitCharacters(entry.kind),
Expand All @@ -53,14 +49,17 @@ export function asCompletionItem(entry: tsp.CompletionEntry, file: string, posit
item.sortText = '\uffff' + entry.sortText;
}

const { sourceDisplay, isSnippet } = entry;
if (sourceDisplay) {
item.detail = asPlainText(sourceDisplay);
const { isSnippet, sourceDisplay } = entry;
if (isSnippet && !features.completionSnippets) {
return null;
}

if (entry.isImportStatementCompletion || isSnippet || item.kind === lsp.CompletionItemKind.Function || item.kind === lsp.CompletionItemKind.Method) {
if (features.completionSnippets && (isSnippet || entry.isImportStatementCompletion || item.kind === lsp.CompletionItemKind.Function || item.kind === lsp.CompletionItemKind.Method)) {
// Import statements, Functions and Methods can result in a snippet completion when resolved.
item.insertTextFormat = lsp.InsertTextFormat.Snippet;
}
if (sourceDisplay) {
item.detail = asPlainText(sourceDisplay);
}

let insertText = entry.insertText;
let replacementRange = entry.replacementSpan && asRange(entry.replacementSpan);
Expand Down Expand Up @@ -192,7 +191,7 @@ function asCommitCharacters(kind: ScriptElementKind): string[] | undefined {
}

export async function asResolvedCompletionItem(
item: lsp.CompletionItem, details: tsp.CompletionEntryDetails, client: TspClient, options: CompletionOptions
item: lsp.CompletionItem, details: tsp.CompletionEntryDetails, client: TspClient, options: CompletionOptions, features: SupportedFeatures
): Promise<lsp.CompletionItem> {
item.detail = asDetail(details);
item.documentation = asDocumentation(details);
Expand All @@ -201,8 +200,7 @@ export async function asResolvedCompletionItem(
item.additionalTextEdits = asAdditionalTextEdits(details.codeActions, filepath);
item.command = asCommand(details.codeActions, item.data.file);
}
if (options.completeFunctionCalls && item.insertTextFormat === lsp.InsertTextFormat.Snippet
&& (item.kind === lsp.CompletionItemKind.Function || item.kind === lsp.CompletionItemKind.Method)) {
if (features.completionSnippets && options.completeFunctionCalls && (item.kind === lsp.CompletionItemKind.Function || item.kind === lsp.CompletionItemKind.Method)) {
const { line, offset } = item.data;
const position = typeConverters.Position.fromLocation({ line, offset });
const shouldCompleteFunction = await isValidFunctionCompletionContext(filepath, position, client);
Expand Down Expand Up @@ -251,6 +249,7 @@ function createSnippetOfFunctionCall(item: lsp.CompletionItem, detail: tsp.Compl
snippet.appendText(')');
snippet.appendTabstop(0);
item.insertText = snippet.value;
item.insertTextFormat = lsp.InsertTextFormat.Snippet;
}

function getParameterListParts(displayParts: ReadonlyArray<tsp.SymbolDisplayPart>): ParameterListParts {
Expand Down
9 changes: 5 additions & 4 deletions src/diagnostic-queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Logger } from './logger.js';
import { pathToUri, toDiagnostic } from './protocol-translation.js';
import { EventTypes } from './tsp-command-types.js';
import { LspDocuments } from './document.js';
import { SupportedFeatures } from './ts-protocol.js';

class FileDiagnostics {
private readonly diagnosticsPerKind = new Map<EventTypes, tsp.Diagnostic[]>();
Expand All @@ -20,7 +21,7 @@ class FileDiagnostics {
protected readonly uri: string,
protected readonly publishDiagnostics: (params: lsp.PublishDiagnosticsParams) => void,
protected readonly documents: LspDocuments,
protected readonly publishDiagnosticsCapabilities: lsp.TextDocumentClientCapabilities['publishDiagnostics']
protected readonly features: SupportedFeatures
) { }

update(kind: EventTypes, diagnostics: tsp.Diagnostic[]): void {
Expand All @@ -36,7 +37,7 @@ class FileDiagnostics {
const result: lsp.Diagnostic[] = [];
for (const diagnostics of this.diagnosticsPerKind.values()) {
for (const diagnostic of diagnostics) {
result.push(toDiagnostic(diagnostic, this.documents, this.publishDiagnosticsCapabilities));
result.push(toDiagnostic(diagnostic, this.documents, this.features));
}
}
return result;
Expand All @@ -50,7 +51,7 @@ export class DiagnosticEventQueue {
constructor(
protected readonly publishDiagnostics: (params: lsp.PublishDiagnosticsParams) => void,
protected readonly documents: LspDocuments,
protected readonly publishDiagnosticsCapabilities: lsp.TextDocumentClientCapabilities['publishDiagnostics'],
protected readonly features: SupportedFeatures,
protected readonly logger: Logger
) { }

Expand All @@ -67,7 +68,7 @@ export class DiagnosticEventQueue {
}
const uri = pathToUri(file, this.documents);
const diagnosticsForFile = this.diagnostics.get(uri) || new FileDiagnostics(
uri, this.publishDiagnostics, this.documents, this.publishDiagnosticsCapabilities);
uri, this.publishDiagnostics, this.documents, this.features);
diagnosticsForFile.update(kind, diagnostics);
this.diagnostics.set(uri, diagnosticsForFile);
}
Expand Down
118 changes: 112 additions & 6 deletions src/lsp-server.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import * as chai from 'chai';
import fs from 'fs-extra';
import * as lsp from 'vscode-languageserver';
import * as lspcalls from './lsp-protocol.calls.proposed.js';
import { uri, createServer, position, lastPosition, filePath, getDefaultClientCapabilities, positionAfter, readContents, TestLspServer, toPlatformEOL } from './test-utils.js';
import { uri, createServer, position, lastPosition, filePath, positionAfter, readContents, TestLspServer, toPlatformEOL } from './test-utils.js';
import { TextDocument } from 'vscode-languageserver-textdocument';
import { Commands } from './commands.js';
import { TypeScriptWorkspaceSettings } from './ts-protocol.js';
Expand Down Expand Up @@ -1635,9 +1635,11 @@ describe('diagnostics (no client support)', () => {
let localServer: TestLspServer;

before(async () => {
// Remove the "textDocument.publishDiagnostics" client capability.
const clientCapabilitiesOverride = getDefaultClientCapabilities();
delete clientCapabilitiesOverride.textDocument?.publishDiagnostics;
const clientCapabilitiesOverride: lsp.ClientCapabilities = {
textDocument: {
publishDiagnostics: undefined
}
};
localServer = await createServer({
rootUri: null,
publishDiagnostics: args => diagnostics.set(args.uri, args),
Expand All @@ -1657,7 +1659,7 @@ describe('diagnostics (no client support)', () => {
localServer.shutdown();
});

it('diagnostics are published', async () => {
it('diagnostic tags are not returned', async () => {
const doc = {
uri: uri('diagnosticsBar.ts'),
languageId: 'typescript',
Expand All @@ -1676,7 +1678,8 @@ describe('diagnostics (no client support)', () => {
await new Promise(resolve => setTimeout(resolve, 200));
const resultsForFile = diagnostics.get(doc.uri);
assert.isDefined(resultsForFile);
assert.strictEqual(resultsForFile?.diagnostics.length, 1);
assert.strictEqual(resultsForFile!.diagnostics.length, 1);
assert.notProperty(resultsForFile!.diagnostics[0], 'tags');
});
});

Expand Down Expand Up @@ -1766,3 +1769,106 @@ describe('inlayHints', () => {
assert.deepStrictEqual(inlayHints[0].position, { line: 1, character: 29 });
});
});

describe('completions without client snippet support', () => {
let localServer: TestLspServer;

before(async () => {
const clientCapabilitiesOverride: lsp.ClientCapabilities = {
textDocument: {
completion: {
completionItem: {
snippetSupport: false
}
}
}
};
localServer = await createServer({
rootUri: null,
publishDiagnostics: args => diagnostics.set(args.uri, args),
clientCapabilitiesOverride
});
});

beforeEach(() => {
localServer.closeAll();
// "closeAll" triggers final publishDiagnostics with an empty list so clear last.
diagnostics.clear();
localServer.workspaceEdits = [];
});

after(() => {
localServer.closeAll();
localServer.shutdown();
});

it('resolves completion for method completion does not contain snippet', async () => {
const doc = {
uri: uri('bar.ts'),
languageId: 'typescript',
version: 1,
text: `
import fs from 'fs'
fs.readFile
`
};
localServer.didOpenTextDocument({ textDocument: doc });
const proposals = await localServer.completion({ textDocument: doc, position: positionAfter(doc, 'readFile') });
assert.isNotNull(proposals);
const completion = proposals!.items.find(completion => completion.label === 'readFile');
assert.notEqual(completion!.insertTextFormat, lsp.InsertTextFormat.Snippet);
assert.strictEqual(completion!.label, 'readFile');
const resolvedItem = await localServer.completionResolve(completion!);
assert.strictEqual(resolvedItem!.label, 'readFile');
assert.strictEqual(resolvedItem.insertText, undefined);
assert.strictEqual(resolvedItem.insertTextFormat, undefined);
localServer.didCloseTextDocument({ textDocument: doc });
});

it('does not include snippet completions for element prop', async () => {
const doc = {
uri: uri('jsx', 'app.tsx'),
languageId: 'typescriptreact',
version: 1,
text: readContents(filePath('jsx', 'app.tsx'))
};
localServer.didOpenTextDocument({
textDocument: doc
});

const completion = await localServer.completion({ textDocument: doc, position: position(doc, 'title') });
assert.isNotNull(completion);
const item = completion!.items.find(i => i.label === 'title');
assert.isUndefined(item);
});

it('does not include snippet completions for object methods', async () => {
const doc = {
uri: uri('foo.ts'),
languageId: 'typescript',
version: 1,
text: `
interface IFoo {
bar(x: number): void;
}
const obj: IFoo = {
/*a*/
}
`
};
localServer.didOpenTextDocument({ textDocument: doc });
const proposals = await localServer.completion({
textDocument: doc,
position: positionAfter(doc, '/*a*/')
});
assert.isNotNull(proposals);
assert.lengthOf(proposals!.items, 1);
assert.deepInclude(
proposals!.items[0],
{
label: 'bar',
kind: 2
}
);
});
});
47 changes: 32 additions & 15 deletions src/lsp-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,12 +194,6 @@ export class LspServer {
this.options.lspClient.setClientCapabilites(clientCapabilities);
this._loadingIndicator = new ServerInitializingIndicator(this.options.lspClient);
this.workspaceRoot = this.initializeParams.rootUri ? uriToPath(this.initializeParams.rootUri) : this.initializeParams.rootPath || undefined;
this.diagnosticQueue = new DiagnosticEventQueue(
diagnostics => this.options.lspClient.publishDiagnostics(diagnostics),
this.documents,
clientCapabilities.textDocument?.publishDiagnostics,
this.logger
);

const userInitializationOptions: TypeScriptInitializationOptions = this.initializeParams.initializationOptions || {};
const { disableAutomaticTypingAcquisition, hostInfo, maxTsServerMemory, npmLocation, locale } = userInitializationOptions;
Expand Down Expand Up @@ -228,17 +222,32 @@ export class LspServer {
...userInitializationOptions.preferences
};

if (userPreferences.useLabelDetailsInCompletionEntries
&& clientCapabilities.textDocument?.completion?.completionItem?.labelDetailsSupport
&& typescriptVersion.version?.gte(API.v470)) {
this.features.labelDetails = true;
const { textDocument } = clientCapabilities;
const completionCapabilities = textDocument?.completion;
if (completionCapabilities?.completionItem) {
if (userPreferences.useLabelDetailsInCompletionEntries && completionCapabilities.completionItem.labelDetailsSupport
&& typescriptVersion.version?.gte(API.v470)) {
this.features.completionLabelDetails = true;
}
if (completionCapabilities.completionItem.snippetSupport) {
this.features.completionSnippets = true;
}
if (textDocument?.publishDiagnostics?.tagSupport) {
this.features.diagnosticsTagSupport = true;
}
}

const finalPreferences: TypeScriptInitializationOptions['preferences'] = {
...userPreferences,
...{ useLabelDetailsInCompletionEntries: this.features.labelDetails }
...{ useLabelDetailsInCompletionEntries: this.features.completionLabelDetails }
};

this.diagnosticQueue = new DiagnosticEventQueue(
diagnostics => this.options.lspClient.publishDiagnostics(diagnostics),
this.documents,
this.features,
this.logger
);
this._tspClient = new TspClient({
tsserverPath: typescriptVersion.tsServerPath,
logFile,
Expand Down Expand Up @@ -657,9 +666,17 @@ export class LspServer {
triggerKind: params.context?.triggerKind
}));
const { body } = result;
const completions = (body ? body.entries : [])
.filter(entry => entry.kind !== 'warning')
.map(entry => asCompletionItem(entry, file, params.position, document, this.features));
const completions: lsp.CompletionItem[] = [];
for (const entry of body?.entries ?? []) {
if (entry.kind === 'warning') {
continue;
}
const completion = asCompletionItem(entry, file, params.position, document, this.features);
if (!completion) {
continue;
}
completions.push(completion);
}
return lsp.CompletionList.create(completions, body?.isIncomplete);
} catch (error) {
if (error.message === 'No content available.') {
Expand All @@ -681,7 +698,7 @@ export class LspServer {
if (!details) {
return item;
}
return asResolvedCompletionItem(item, details, this.tspClient, this.workspaceConfiguration.completions || {});
return asResolvedCompletionItem(item, details, this.tspClient, this.workspaceConfiguration.completions || {}, this.features);
}

async hover(params: lsp.TextDocumentPositionParams): Promise<lsp.Hover> {
Expand Down
9 changes: 3 additions & 6 deletions src/protocol-translation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import * as lsp from 'vscode-languageserver';
import type tsp from 'typescript/lib/protocol.d.js';
import vscodeUri from 'vscode-uri';
import { LspDocuments } from './document.js';
import { SupportedFeatures } from './ts-protocol.js';

const RE_PATHSEP_WINDOWS = /\\/g;

Expand Down Expand Up @@ -132,11 +133,7 @@ function toDiagnosticSeverity(category: string): lsp.DiagnosticSeverity {
}
}

export function toDiagnostic(
diagnostic: tsp.Diagnostic,
documents: LspDocuments | undefined,
publishDiagnosticsCapabilities: lsp.TextDocumentClientCapabilities['publishDiagnostics']
): lsp.Diagnostic {
export function toDiagnostic(diagnostic: tsp.Diagnostic, documents: LspDocuments | undefined, features: SupportedFeatures): lsp.Diagnostic {
const lspDiagnostic: lsp.Diagnostic = {
range: {
start: toPosition(diagnostic.start),
Expand All @@ -148,7 +145,7 @@ export function toDiagnostic(
source: diagnostic.source || 'typescript',
relatedInformation: asRelatedInformation(diagnostic.relatedInformation, documents)
};
if (publishDiagnosticsCapabilities?.tagSupport) {
if (features.diagnosticsTagSupport) {
lspDiagnostic.tags = getDiagnosticTags(diagnostic);
}
return lspDiagnostic;
Expand Down
Loading