Skip to content

Commit

Permalink
feat: support inlay hints
Browse files Browse the repository at this point in the history
close #452
  • Loading branch information
johnsoncodehk committed Apr 17, 2022
1 parent 8ff7c89 commit ae8821f
Show file tree
Hide file tree
Showing 12 changed files with 157 additions and 0 deletions.
1 change: 1 addition & 0 deletions extensions/vscode-vue-language-features/src/common.ts
Expand Up @@ -228,6 +228,7 @@ function getInitializationOptions(
documentLink: true,
codeLens: { showReferencesNotification: true },
semanticTokens: true,
inlayHints: true,
diagnostics: { getDocumentVersionRequest: true },
schemaRequestService: { getDocumentContentRequest: true },
} : {}),
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/types.ts
Expand Up @@ -57,6 +57,7 @@ export interface ServerInitializationOptions {
}
semanticTokens?: boolean
codeAction?: boolean
inlayHints?: boolean;
diagnostics?: boolean | {
/**
* {@link __requests.GetDocumentVersionRequest}
Expand Down
2 changes: 2 additions & 0 deletions packages/typescript-language-service/src/index.ts
Expand Up @@ -23,6 +23,7 @@ import * as semanticTokens from './services/semanticTokens';
import * as foldingRanges from './services/foldingRanges';
import * as callHierarchy from './services/callHierarchy';
import * as implementation from './services/implementation';
import * as inlayHints from './services/inlayHints';
import { TextDocument } from 'vscode-languageserver-textdocument';
import * as shared from '@volar/shared';
import type * as ts from 'typescript/lib/tsserverlibrary';
Expand Down Expand Up @@ -55,6 +56,7 @@ export function createLanguageService(
getEditsForFileRename: fileRename.register(languageService, getValidTextDocument, settings),
getCodeActions: codeActions.register(languageService, getValidTextDocument, settings),
doCodeActionResolve: codeActionResolve.register(languageService, getValidTextDocument, settings),
getInlayHints: inlayHints.register(languageService, getValidTextDocument, settings, ts),

findDocumentHighlights: documentHighlight.register(languageService, getValidTextDocument, ts),
findDocumentSymbols: documentSymbol.register(languageService, getValidTextDocument),
Expand Down
37 changes: 37 additions & 0 deletions packages/typescript-language-service/src/services/inlayHints.ts
@@ -0,0 +1,37 @@
import * as shared from '@volar/shared';
import type * as ts from 'typescript/lib/tsserverlibrary';
import * as vscode from 'vscode-languageserver-protocol';
import type { TextDocument } from 'vscode-languageserver-textdocument';
import type { Settings } from '..';

export function register(
languageService: ts.LanguageService,
getTextDocument: (uri: string) => TextDocument | undefined,
settings: Settings,
ts: typeof import('typescript/lib/tsserverlibrary'),
) {
return async (uri: string, range: vscode.Range) => {

const document = getTextDocument(uri);
if (!document) return;

const preferences = await settings.getPreferences?.(document) ?? {};
const fileName = shared.uriToFsPath(document.uri);
const start = document.offsetAt(range.start);
const end = document.offsetAt(range.end);
const inlayHints = languageService.provideInlayHints(fileName, { start, length: end - start }, preferences);

return inlayHints.map(inlayHint => {
const result = vscode.InlayHint.create(
document.positionAt(inlayHint.position),
inlayHint.text,
inlayHint.kind === ts.InlayHintKind.Type ? vscode.InlayHintKind.Type
: inlayHint.kind === ts.InlayHintKind.Parameter ? vscode.InlayHintKind.Parameter
: undefined,
);
result.paddingLeft = inlayHint.whitespaceBefore;
result.paddingRight = inlayHint.whitespaceAfter;
return result;
});
};
}
4 changes: 4 additions & 0 deletions packages/vue-language-server/src/features/languageFeatures.ts
Expand Up @@ -215,6 +215,10 @@ export function register(
connection.languages.semanticTokens.onRange(async (handler, token, _, resultProgress) => {
return onSemanticTokens(handler, token, resultProgress);
});
connection.languages.inlayHint.on(async handler => {
const languageService = await getLanguageService(handler.textDocument.uri);
return languageService?.getInlayHints(handler.textDocument.uri, handler.range);
});
connection.workspace.onWillRenameFiles(async handler => {

const hasTsFile = handler.files.some(file => file.newUri.endsWith('.vue') || file.newUri.endsWith('.ts') || file.newUri.endsWith('.tsx'));
Expand Down
3 changes: 3 additions & 0 deletions packages/vue-language-server/src/projects.ts
Expand Up @@ -133,6 +133,9 @@ export function createProjects(
if (options.languageFeatures?.semanticTokens) {
connection.languages.semanticTokens.refresh();
}
if (options.languageFeatures?.inlayHints) {
connection.languages.semanticTokens.refresh();
}
}
}
async function updateDiagnostics(docUri?: string) {
Expand Down
Expand Up @@ -109,4 +109,7 @@ export function register(
resolveProvider: true,
};
}
if (features.inlayHints) {
server.inlayHintProvider = true;
}
}
1 change: 1 addition & 0 deletions packages/vue-language-service-types/src/index.ts
Expand Up @@ -75,6 +75,7 @@ export type EmbeddedLanguageServicePlugin = {
getIncomingCalls(item: vscode.CallHierarchyItem): NotNullableResult<vscode.CallHierarchyIncomingCall[]>;
getOutgoingCalls(item: vscode.CallHierarchyItem): NotNullableResult<vscode.CallHierarchyOutgoingCall[]>;
},
getInlayHints?(document: TextDocument, range: vscode.Range): NullableResult<vscode.InlayHint[]>,

// html
findLinkedEditingRanges?(document: TextDocument, position: vscode.Position): NullableResult<vscode.LinkedEditingRanges>;
Expand Down
79 changes: 79 additions & 0 deletions packages/vue-language-service/src/languageFeatures/inlayHints.ts
@@ -0,0 +1,79 @@
import * as shared from '@volar/shared';
import { transformTextEdit } from '@volar/transforms';
import * as vscode from 'vscode-languageserver-protocol';
import type { LanguageServiceRuntimeContext } from '../types';
import { languageFeatureWorker } from '../utils/featureWorkers';

export function register(context: LanguageServiceRuntimeContext) {

return async (uri: string, range: vscode.Range) => {

const document = context.getTextDocument(uri);

if (!document)
return;

const offsetRange = {
start: document.offsetAt(range.start),
end: document.offsetAt(range.end),
};

return languageFeatureWorker(
context,
uri,
range,
(arg, sourceMap) => {

/**
* copy from ./codeActions.ts
*/

if (!sourceMap.embeddedFile.capabilities.codeActions)
return [];

let minStart: number | undefined;
let maxEnd: number | undefined;

for (const mapping of sourceMap.mappings) {
const overlapRange = shared.getOverlapRange2(offsetRange, mapping.sourceRange);
if (overlapRange) {
const embeddedRange = sourceMap.getMappedRange(overlapRange.start, overlapRange.end)?.[0];
if (embeddedRange) {
minStart = minStart === undefined ? embeddedRange.start : Math.min(embeddedRange.start, minStart);
maxEnd = maxEnd === undefined ? embeddedRange.end : Math.max(embeddedRange.end, maxEnd);
}
}
}

if (minStart !== undefined && maxEnd !== undefined) {
return [vscode.Range.create(
sourceMap.mappedDocument.positionAt(minStart),
sourceMap.mappedDocument.positionAt(maxEnd),
)];
}

return [];
},
(plugin, document, arg, sourceMap) => {
return plugin.getInlayHints?.(document, arg);
},
(inlayHints, sourceMap) => inlayHints.map(_inlayHint => {

if (!sourceMap)
return _inlayHint;

const position = sourceMap.getSourceRange(_inlayHint.position, _inlayHint.position, data => !!data.capabilities.completion)?.[0].start;
const edits = _inlayHint.textEdits?.map(textEdit => transformTextEdit(textEdit, range => sourceMap.getSourceRange(range.start, range.end)?.[0])).filter(shared.notEmpty);

if (position) {
return {
..._inlayHint,
position,
edits,
};
}
}).filter(shared.notEmpty),
arr => arr.flat(),
);
}
}
2 changes: 2 additions & 0 deletions packages/vue-language-service/src/languageService.ts
Expand Up @@ -38,6 +38,7 @@ import * as renamePrepare from './languageFeatures/renamePrepare';
import * as signatureHelp from './languageFeatures/signatureHelp';
import * as diagnostics from './languageFeatures/validation';
import * as workspaceSymbol from './languageFeatures/workspaceSymbols';
import * as inlayHints from './languageFeatures/inlayHints';
import { getTsSettings } from './tsConfigs';
import { LanguageServiceHost, LanguageServiceRuntimeContext } from './types';
import { parseVueDocuments } from './vueDocuments';
Expand Down Expand Up @@ -286,6 +287,7 @@ export function createLanguageService(
findWorkspaceSymbols: defineApi(workspaceSymbol.register(context)),
doAutoInsert: defineApi(autoInsert.register(context)),
doExecuteCommand: defineApi(executeCommand.register(context)),
getInlayHints: defineApi(inlayHints.register(context)),
callHierarchy: {
doPrepare: defineApi(_callHierarchy.doPrepare),
getIncomingCalls: defineApi(_callHierarchy.getIncomingCalls),
Expand Down
6 changes: 6 additions & 0 deletions packages/vue-language-service/src/plugins/typescript.ts
Expand Up @@ -191,6 +191,12 @@ export default function (options: {
}
},

getInlayHints(document, range) {
if (isTsDocument(document)) {
return options.getTsLs().getInlayHints(document.uri, range);
}
},

format(document, range, options_2) {
if (isTsDocument(document)) {
return options.getTsLs().doFormatting(document.uri, options_2, range);
Expand Down
18 changes: 18 additions & 0 deletions packages/vue-language-service/src/tsConfigs.ts
Expand Up @@ -75,6 +75,15 @@ export async function getPreferences(
allowIncompleteCompletions: true,
displayPartsForJSDoc: true,

// inlay hints
includeInlayParameterNameHints: getInlayParameterNameHintsPreference(config),
includeInlayParameterNameHintsWhenArgumentMatchesName: !(config.inlayHints?.parameterNames?.suppressWhenArgumentMatchesName ?? true),
includeInlayFunctionParameterTypeHints: config.inlayHints?.parameterTypes?.enabled ?? false,
includeInlayVariableTypeHints: config.inlayHints?.variableTypes?.enabled ?? false,
includeInlayPropertyDeclarationTypeHints: config.inlayHints?.propertyDeclarationTypes?.enabled ?? false,
includeInlayFunctionLikeReturnTypeHints: config.inlayHints?.functionLikeReturnTypes?.enabled ?? false,
includeInlayEnumMemberValueHints: config.inlayHints?.enumMemberValues?.enabled ?? false,

This comment has been minimized.

Copy link
@predragnikolic

predragnikolic May 7, 2022

Contributor

How can a client provide these config options to the server?

I thought it can provide it through initializationOptions, but they do not accept inlayHint options.
Than I looked at workspace settings, but I haven't found any workspace setting for inlay hints here.

This comment has been minimized.

Copy link
@johnsoncodehk

johnsoncodehk May 7, 2022

Author Member

@predragnikolic These configs get by LSP workspace/configuration request. The way the language client provides configs is determined by the IDE. For example, VSCode obtains configs from .vscode/settings.json and provides them to LSP.

https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_configuration

This comment has been minimized.

This comment has been minimized.

Copy link
@predragnikolic

predragnikolic May 7, 2022

Contributor

Thanks @johnsoncodehk , so it is the workspace/configuration request.

I was trying to debug why the server is sending an empty list for the inlayHint request.

So the server asks the client,
and the client does send back the asked configuration:

:: <-- LSP-volar workspace/configuration(2): {'items': [{'section': 'typescript', 'scopeUri': 'file:///home/predragnikolic/Documents/sandbox/my-vue-app/src/main.ts'}]}
:: >>> LSP-volar 2: [{'inlayHints': {'variableTypes': {'enabled': True}, 'parameterTypes': {'enabled': True}, 'parameterNames': {'suppressWhenArgumentMatchesName': True}, 'propertyDeclarationTypes': {'enabled': True}, 'functionLikeReturnTypes': {'enabled': True}, 'enumMemberValues': {'enabled': True}}}]

It turn out to be a mistake that I made.
The client sent the same start and end range
and thus the server always returned an empty array for the inlayHint request :)

Sorry for the bother and thanks for the quick reply!


// custom
includeCompletionsForModuleExports: config.suggest?.autoImports ?? true,
};
Expand Down Expand Up @@ -111,3 +120,12 @@ function getImportModuleSpecifierEndingPreference(config: any) {
function isTypeScriptDocument(doc: TextDocument) {
return ['typescript', 'typescriptreact'].includes(doc.languageId);
}

function getInlayParameterNameHintsPreference(config: any) {
switch (config.inlayHints?.parameterNames?.enabled) {
case 'none': return 'none';
case 'literals': return 'literals';
case 'all': return 'all';
default: return undefined;
}
}

5 comments on commit ae8821f

@Shinigami92
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😲🥰 Happy to test this out on tuesday

@dajpes
Copy link

@dajpes dajpes commented on ae8821f Apr 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How Can I set this up in vscode?

@yaegassy
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dajpes Try setting typescript.inlayHints.* or javascript.inlayHints.* to true

Example settings.json:

{
  // ...snip
  "typescript.inlayHints.enumMemberValues.enabled": true,
  "typescript.inlayHints.functionLikeReturnTypes.enabled": true,
  "typescript.inlayHints.propertyDeclarationTypes.enabled": true,
  "typescript.inlayHints.parameterTypes.enabled": true,
  "typescript.inlayHints.variableTypes.enabled": true,
  "typescript.inlayHints.parameterNames.suppressWhenArgumentMatchesName": true,
  "javascript.inlayHints.variableTypes.enabled": true,
  "javascript.inlayHints.parameterTypes.enabled": true,
  "javascript.inlayHints.enumMemberValues.enabled": true,
  "javascript.inlayHints.functionLikeReturnTypes.enabled": true,
  "javascript.inlayHints.propertyDeclarationTypes.enabled": true,
  "javascript.inlayHints.parameterNames.suppressWhenArgumentMatchesName": true,
  // ...snip
}

@dajpes
Copy link

@dajpes dajpes commented on ae8821f Apr 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @yaegassy , you method actually works, but only if I have enabled vscode built in typescript extension.

Since I have that disabled (because volar required it so), then I'm not able to turn the inlay hints on.

So, in other works is there a way to enable vscode inlay hints with volar extension, but having vscode built in typescript extension off?

cc. @johnsoncodehk

@Ragura
Copy link

@Ragura Ragura commented on ae8821f May 11, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @yaegassy , you method actually works, but only if I have enabled vscode built in typescript extension.

Since I have that disabled (because volar required it so), then I'm not able to turn the inlay hints on.

So, in other works is there a way to enable vscode inlay hints with volar extension, but having vscode built in typescript extension off?

cc. @johnsoncodehk

You need to add the settings to your project's VSCode settings.json file. The typescript settings are only recognized in the workspace settings.

Please sign in to comment.