Skip to content

Commit

Permalink
feat: add support for CompletionItem.labelDetails (#534)
Browse files Browse the repository at this point in the history
  • Loading branch information
rchl committed Aug 6, 2022
1 parent 158ca97 commit 3c140d9
Show file tree
Hide file tree
Showing 7 changed files with 93 additions and 15 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,10 @@ interface UserPreferences {
/**
* Indicates whether {@link CompletionEntry.labelDetails completion entry label details} are supported.
* If not, contents of `labelDetails` may be included in the {@link CompletionEntry.name} property.
* Only supported if the client supports `textDocument.completion.completionItem.labelDetailsSupport` capability
* and a compatible TypeScript version is used.
* @since 4.7.2
* @default false
* @default true
*/
useLabelDetailsInCompletionEntries: boolean;
/**
Expand Down
5 changes: 3 additions & 2 deletions src/completion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { CommandTypes, KindModifiers, ScriptElementKind } from './tsp-command-ty
import { asRange, toTextEdit, asPlainText, asDocumentation, normalizePath } from './protocol-translation';
import { Commands } from './commands';
import { TspClient } from './tsp-client';
import { CompletionOptions, DisplayPartKind } from './ts-protocol';
import { CompletionOptions, DisplayPartKind, SupportedFeatures } from './ts-protocol';
import SnippetString from './utils/SnippetString';
import * as typeConverters from './utils/typeConverters';

Expand All @@ -25,9 +25,10 @@ interface ParameterListParts {
readonly hasOptionalParameters: boolean;
}

export function asCompletionItem(entry: tsp.CompletionEntry, file: string, position: lsp.Position, document: LspDocument): TSCompletionItem {
export function asCompletionItem(entry: tsp.CompletionEntry, file: string, position: lsp.Position, document: LspDocument, features: SupportedFeatures): TSCompletionItem {
const item: TSCompletionItem = {
label: entry.name,
...features.labelDetails ? { labelDetails: entry.labelDetails } : {},
kind: asCompletionItemKind(entry.kind),
sortText: entry.sortText,
commitCharacters: asCommitCharacters(entry.kind),
Expand Down
47 changes: 45 additions & 2 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 * as fs from 'fs-extra';
import * as lsp from 'vscode-languageserver/node';
import * as lspcalls from './lsp-protocol.calls.proposed';
import { uri, createServer, position, lastPosition, filePath, getDefaultClientCapabilities, positionAfter, readContents, TestLspServer } from './test-utils';
import { uri, createServer, position, lastPosition, filePath, getDefaultClientCapabilities, positionAfter, readContents, TestLspServer, toPlatformEOL } from './test-utils';
import { TextDocument } from 'vscode-languageserver-textdocument';
import { Commands } from './commands';
import { TypeScriptWorkspaceSettings } from './ts-protocol';
Expand Down Expand Up @@ -335,7 +335,7 @@ describe('completion', () => {
function test(value: "fs/read" | "hello/world") {
return true;
}
test("fs/")
`
};
Expand All @@ -359,6 +359,49 @@ describe('completion', () => {
newText: 'fs/read'
});
});

it('includes labelDetails with useLabelDetailsInCompletionEntries enabled', async () => {
const doc = {
uri: uri('foo.ts'),
languageId: 'typescript',
version: 1,
text: `
interface IFoo {
bar(x: number): void;
}
const obj: IFoo = {
/*a*/
}
`
};
server.didOpenTextDocument({ textDocument: doc });
const proposals = await server.completion({
textDocument: doc,
position: positionAfter(doc, '/*a*/')
});
assert.isNotNull(proposals);
assert.lengthOf(proposals!.items, 2);
assert.deepInclude(
proposals!.items[0],
{
label: 'bar',
kind: 2,
insertTextFormat: 2
}
);
assert.deepInclude(
proposals!.items[1],
{
label: 'bar',
labelDetails: {
detail: '(x)'
},
kind: 2,
insertTextFormat: 2,
insertText: toPlatformEOL('bar(x) {\n $0\n},')
}
);
});
});

describe('diagnostics', () => {
Expand Down
34 changes: 24 additions & 10 deletions src/lsp-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import tsp from 'typescript/lib/protocol';
import * as fs from 'fs-extra';
import debounce from 'p-debounce';

import API from './utils/api';
import { CommandTypes, EventTypes } from './tsp-command-types';
import { Logger, PrefixingLogger } from './logger';
import { TspClient } from './tsp-client';
Expand All @@ -31,7 +32,7 @@ import { Commands } from './commands';
import { provideQuickFix } from './quickfix';
import { provideRefactors } from './refactor';
import { provideOrganizeImports } from './organize-imports';
import { TypeScriptInitializeParams, TypeScriptInitializationOptions, TypeScriptInitializeResult, TypeScriptWorkspaceSettings, TypeScriptWorkspaceSettingsLanguageSettings } from './ts-protocol';
import { TypeScriptInitializeParams, TypeScriptInitializationOptions, TypeScriptInitializeResult, TypeScriptWorkspaceSettings, TypeScriptWorkspaceSettingsLanguageSettings, SupportedFeatures } from './ts-protocol';
import { collectDocumentSymbols, collectSymbolInformation } from './document-symbol';
import { computeCallers, computeCallees } from './calls';
import { IServerOptions } from './utils/configuration';
Expand Down Expand Up @@ -69,7 +70,7 @@ const DEFAULT_TSSERVER_PREFERENCES: Required<tsp.UserPreferences> = {
providePrefixAndSuffixTextForRename: true,
provideRefactorNotApplicableReason: false,
quotePreference: 'auto',
useLabelDetailsInCompletionEntries: false
useLabelDetailsInCompletionEntries: true
};

class ServerInitializingIndicator {
Expand Down Expand Up @@ -119,6 +120,7 @@ export class LspServer {
private workspaceRoot: string | undefined;
private typeScriptAutoFixProvider: TypeScriptAutoFixProvider;
private loadingIndicator: ServerInitializingIndicator;
private features: SupportedFeatures = {};

private readonly documents = new LspDocuments();

Expand Down Expand Up @@ -174,13 +176,9 @@ export class LspServer {

const userInitializationOptions: TypeScriptInitializationOptions = this.initializeParams.initializationOptions || {};
const { disableAutomaticTypingAcquisition, hostInfo, maxTsServerMemory, npmLocation, locale } = userInitializationOptions;
const { logVerbosity, plugins, preferences }: TypeScriptInitializationOptions = {
const { logVerbosity, plugins }: TypeScriptInitializationOptions = {
logVerbosity: userInitializationOptions.logVerbosity || this.options.tsserverLogVerbosity,
plugins: userInitializationOptions.plugins || [],
preferences: {
...DEFAULT_TSSERVER_PREFERENCES,
...userInitializationOptions.preferences
}
plugins: userInitializationOptions.plugins || []
};

const logFile = this.getLogFile(logVerbosity);
Expand All @@ -198,6 +196,22 @@ export class LspServer {
throw Error('Could not find a valid tsserver executable in the workspace or in the $PATH. Please ensure that the "typescript" dependency is installed in either location. Exiting.');
}

const userPreferences: TypeScriptInitializationOptions['preferences'] = {
...DEFAULT_TSSERVER_PREFERENCES,
...userInitializationOptions.preferences
};

if (userPreferences.useLabelDetailsInCompletionEntries
&& clientCapabilities.textDocument?.completion?.completionItem?.labelDetailsSupport
&& typescriptVersion.version?.gte(API.v470)) {
this.features.labelDetails = true;
}

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

this.tspClient = new TspClient({
tsserverPath: typescriptVersion.tsServerPath,
logFile,
Expand Down Expand Up @@ -240,7 +254,7 @@ export class LspServer {
// We can use \n here since the editor should normalize later on to its line endings.
newLineCharacter: '\n'
},
preferences
preferences: finalPreferences
}),
this.tspClient.request(CommandTypes.CompilerOptionsForInferredProjects, {
options: {
Expand Down Expand Up @@ -615,7 +629,7 @@ export class LspServer {
const { body } = result;
const completions = (body ? body.entries : [])
.filter(entry => entry.kind !== 'warning')
.map(entry => asCompletionItem(entry, file, params.position, document));
.map(entry => asCompletionItem(entry, file, params.position, document, this.features));
return lsp.CompletionList.create(completions, body?.isIncomplete);
} catch (error) {
if (error.message === 'No content available.') {
Expand Down
13 changes: 13 additions & 0 deletions src/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*/

import { platform } from 'os';
import * as path from 'path';
import * as fs from 'fs';
import * as lsp from 'vscode-languageserver/node';
Expand All @@ -19,6 +20,11 @@ const CONSOLE_LOG_LEVEL = ConsoleLogger.toMessageTypeLevel(process.env.CONSOLE_L
export function getDefaultClientCapabilities(): lsp.ClientCapabilities {
return {
textDocument: {
completion: {
completionItem: {
labelDetailsSupport: true
}
},
documentSymbol: {
hierarchicalDocumentSymbolSupport: true
},
Expand Down Expand Up @@ -69,6 +75,13 @@ export function lastPosition(document: lsp.TextDocumentItem, match: string): lsp
return positionAt(document, document.text.lastIndexOf(match));
}

export function toPlatformEOL(text: string): string {
if (platform() === 'win32') {
return text.replace(/(?!\r)\n/g, '\r\n');
}
return text;
}

export class TestLspServer extends LspServer {
workspaceEdits: lsp.ApplyWorkspaceEditParams[] = [];
}
Expand Down
4 changes: 4 additions & 0 deletions src/ts-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ export class DisplayPartKind {
public static readonly text = 'text';
}

export interface SupportedFeatures {
labelDetails?: boolean;
}

export interface TypeScriptPlugin {
name: string;
location: string;
Expand Down
1 change: 1 addition & 0 deletions src/utils/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export default class API {
public static readonly v420 = API.fromSimpleString('4.2.0');
public static readonly v430 = API.fromSimpleString('4.3.0');
public static readonly v440 = API.fromSimpleString('4.4.0');
public static readonly v470 = API.fromSimpleString('4.7.0');

public static fromVersionString(versionString: string): API {
let version = semver.valid(versionString);
Expand Down

0 comments on commit 3c140d9

Please sign in to comment.