Skip to content
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

feat: Introduce InlayHints #532

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
11 changes: 11 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@
"--extensionDevelopmentPath=${workspaceFolder}/packages/vscode",
"${workspaceFolder}/test-packages"
]
},
{
"type": "node",
"request": "launch",
"name": "Debug Current Test File",
Copy link
Member

Choose a reason for hiding this comment

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

Nice!

"autoAttachChildProcesses": true,
"skipFiles": ["<node_internals>/**", "**/node_modules/**"],
"program": "${workspaceFolder}/node_modules/vitest/vitest.mjs",
"args": ["run", "${relativeFile}"],
"smartStep": true,
"console": "integratedTerminal"
}
]
}
84 changes: 84 additions & 0 deletions packages/core/__tests__/language-server/inlay.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { Project } from 'glint-monorepo-test-utils';
import { describe, beforeEach, afterEach, test, expect } from 'vitest';
import { Position, Range } from 'vscode-languageserver';

describe('Language Server: iInlays', () => {
let project!: Project;

beforeEach(async () => {
project = await Project.create();
});

afterEach(async () => {
await project.destroy();
});

test('it provides inlays for return types when preference is set', async () => {
project.setGlintConfig({ environment: 'ember-template-imports' });
let content = 'const bar = () => true;';
project.write('foo.gts', content);
let server = project.startLanguageServer();

const inlays = server.getInlayHints(
{
textDocument: {
uri: project.fileURI('foo.gts'),
},
range: Range.create(Position.create(0, 0), Position.create(0, content.length)),
},
{
includeInlayFunctionLikeReturnTypeHints: true,
}
);

expect(inlays.length).toBe(1);
expect(inlays[0].kind).toBe(1);
expect(inlays[0].label).toBe(': boolean');
});

test('it provides inlays for variable types when preference is set', async () => {
project.setGlintConfig({ environment: 'ember-template-imports' });
let content = 'const bar = globalThis.thing ?? null;';
project.write('foo.gts', content);
let server = project.startLanguageServer();

const inlays = server.getInlayHints(
{
textDocument: {
uri: project.fileURI('foo.gts'),
},
range: Range.create(Position.create(0, 0), Position.create(0, content.length)),
},
{
includeInlayVariableTypeHints: true,
}
);

expect(inlays.length).toBe(1);
expect(inlays[0].kind).toBe(1);
expect(inlays[0].label).toBe(': any');
});

test('it provides inlays for property types when preference is set', async () => {
project.setGlintConfig({ environment: 'ember-template-imports' });
let content = 'class Foo { date = Date.now() }';
project.write('foo.gts', content);
let server = project.startLanguageServer();

const inlays = server.getInlayHints(
{
textDocument: {
uri: project.fileURI('foo.gts'),
},
range: Range.create(Position.create(0, 0), Position.create(0, content.length)),
},
{
includeInlayPropertyDeclarationTypeHints: true,
}
);

expect(inlays.length).toBe(1);
expect(inlays[0].kind).toBe(1);
expect(inlays[0].label).toBe(': number');
});
});
8 changes: 8 additions & 0 deletions packages/core/src/language-server/binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const capabilities: ServerCapabilities = {
},
referencesProvider: true,
hoverProvider: true,
inlayHintProvider: true,
codeActionProvider: {
codeActionKinds: [CodeActionKind.QuickFix],
},
Expand Down Expand Up @@ -113,6 +114,13 @@ export function bindLanguageServerPool({
});
});

connection.languages.inlayHint.on((hint) => {
return pool.withServerForURI(hint.textDocument.uri, ({ server }) => {
let language = server.getLanguageType(hint.textDocument.uri);
return server.getInlayHints(hint, configManager.getUserSettingsFor(language));
});
});

connection.onCodeAction(({ textDocument, range, context }) => {
return pool.withServerForURI(textDocument.uri, ({ server }) => {
// The user actually asked for the fix
Expand Down
74 changes: 74 additions & 0 deletions packages/core/src/language-server/glint-language-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ import {
TextDocumentEdit,
OptionalVersionedTextDocumentIdentifier,
TextEdit,
InlayHint,
InlayHintParams,
Position as LSPPosition,
InlayHintKind,
} from 'vscode-languageserver';
import DocumentCache from '../common/document-cache.js';
import { Position, positionToOffset } from './util/position.js';
Expand Down Expand Up @@ -381,6 +385,42 @@ export default class GlintLanguageServer {
}
}

public getInlayHints(hint: InlayHintParams, preferences: ts.UserPreferences = {}): InlayHint[] {
let { uri } = hint.textDocument;
let { range } = hint;
let fileName = uriToFilePath(uri);

let { transformedStart, transformedEnd, transformedFileName } =
this.getTransformedOffsetsFromPositions(
uri,
{
line: range.start.line,
character: range.start.character,
},
{
line: range.end.line,
character: range.end.character,
}
);
Comment on lines +394 to +404
Copy link
Member

Choose a reason for hiding this comment

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

Can this just be getTransformedOffsetsFromPositions(uri, range.start, range.end)?


const inlayHints = this.service.provideInlayHints(
transformedFileName,
{
start: transformedStart,
length: transformedEnd - transformedStart,
},
preferences
);

let content = this.documents.getDocumentContents(fileName);

return inlayHints
.map((tsInlayHint) => {
return this.transformTSInlayToLSPInlay(tsInlayHint, transformedFileName, content);
})
.filter(isHint);
}

public getCodeActions(
uri: string,
actionKind: string,
Expand All @@ -404,6 +444,32 @@ export default class GlintLanguageServer {
return this.glintConfig.environment.isTypedScript(file) ? 'typescript' : 'javascript';
}

private transformTSInlayToLSPInlay(
hint: ts.InlayHint,
fileName: string,
contents: string
): InlayHint | undefined {
let { position, text } = hint;
let { originalStart } = this.transformManager.getOriginalRange(
fileName,
position,
position + text.length
);

const { line, character } = offsetToPosition(contents, originalStart);

let kind =
hint.kind === 'Parameter'
? InlayHintKind.Parameter
: hint.kind === 'Type'
? InlayHintKind.Type
: undefined; // enums are not supported by LSP;

if (isInlayHintKind(kind)) {
return InlayHint.create(LSPPosition.create(line, character), hint.text, kind);
}
}

private applyCodeAction(
uri: string,
range: Range,
Expand Down Expand Up @@ -651,3 +717,11 @@ export default class GlintLanguageServer {
function onlyNumbers(entry: number | undefined): entry is number {
return entry !== undefined;
}

function isHint(hint: InlayHint | undefined): hint is InlayHint {
return hint !== undefined;
}

function isInlayHintKind(kind: number | undefined): kind is InlayHintKind {
return kind !== undefined;
}
39 changes: 13 additions & 26 deletions packages/vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ import {
window,
commands,
workspace,
WorkspaceConfiguration,
} from 'vscode';
import { Disposable, LanguageClient, ServerOptions } from 'vscode-languageclient/node.js';
import type { Request, GetIRRequest } from '@glint/core/lsp-messages';
import { intoFormatting } from './formatting';
import { intoPreferences } from './preferences';

///////////////////////////////////////////////////////////////////////////////
// Setup and extension lifecycle
Expand Down Expand Up @@ -109,28 +110,25 @@ async function addWorkspaceFolder(

let serverOptions: ServerOptions = { module: serverPath };

const typescriptFormatOptions = getOptions(workspace.getConfiguration('typescript'), 'format');
const typescriptUserPreferences = getOptions(
workspace.getConfiguration('typescript'),
'preferences'
);
const javascriptFormatOptions = getOptions(workspace.getConfiguration('javascript'), 'format');
const javascriptUserPreferences = getOptions(
workspace.getConfiguration('javascript'),
'preferences'
);
let typescriptConfig = workspace.getConfiguration('typescript');
let typescriptFormatOptions = workspace.getConfiguration('typescript.format');
let typescriptUserPreferences = workspace.getConfiguration('typescript.preferences');

let javaScriptConfig = workspace.getConfiguration('javascript');
let javascriptFormatOptions = workspace.getConfiguration('javascript.format');
let javascriptUserPreferences = workspace.getConfiguration('javascript.preferences');

let client = new LanguageClient('glint', 'Glint', serverOptions, {
workspaceFolder,
outputChannel,
initializationOptions: {
javascript: {
format: javascriptFormatOptions,
preferences: javascriptUserPreferences,
format: intoFormatting(javascriptFormatOptions),
preferences: intoPreferences(javaScriptConfig, javascriptUserPreferences),
},
typescript: {
format: typescriptFormatOptions,
preferences: typescriptUserPreferences,
format: intoFormatting(typescriptFormatOptions),
preferences: intoPreferences(typescriptConfig, typescriptUserPreferences),
},
},
documentSelector: [{ scheme: 'file', pattern: `${folderPath}/${filePattern}` }],
Expand Down Expand Up @@ -191,14 +189,3 @@ function createConfigWatcher(): Disposable {
function requestKey<R extends Request<string, unknown>>(name: R['name']): R['type'] {
return name as unknown as R['type'];
}

// Loads the TypeScript and JavaScript formating options from the workspace and subsets them to
// pass to the language server.
function getOptions(config: WorkspaceConfiguration, key: string): object {
const formatOptions = config.get<object>(key);
if (formatOptions) {
return formatOptions;
}

return {};
}
64 changes: 64 additions & 0 deletions packages/vscode/src/formatting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { type WorkspaceConfiguration, window } from 'vscode';
import type * as ts from 'typescript/lib/tsserverlibrary';

// vscode does not hold formatting config with the same interface as typescript
// the following maps the vscode formatting options into what typescript expects
// This is heavily borrowed from how the TypeScript works in vscode
// https://github.com/microsoft/vscode/blob/c04c0b43470c3c743468a5e5e51f036123503452/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts#L133
export function intoFormatting(
config: WorkspaceConfiguration
): ts.server.protocol.FormatCodeSettings {
let editorOptions = window.activeTextEditor?.options;
let tabSize = typeof editorOptions?.tabSize === 'string' ? undefined : editorOptions?.tabSize;
let insertSpaces =
typeof editorOptions?.insertSpaces === 'string' ? undefined : editorOptions?.insertSpaces;

return {
tabSize,
indentSize: tabSize,
convertTabsToSpaces: insertSpaces,
// We can use \n here since the editor normalizes later on to its line endings.
newLineCharacter: '\n',
insertSpaceAfterCommaDelimiter: config.get<boolean>('insertSpaceAfterCommaDelimiter'),
insertSpaceAfterConstructor: config.get<boolean>('insertSpaceAfterConstructor'),
insertSpaceAfterSemicolonInForStatements: config.get<boolean>(
'insertSpaceAfterSemicolonInForStatements'
),
insertSpaceBeforeAndAfterBinaryOperators: config.get<boolean>(
'insertSpaceBeforeAndAfterBinaryOperators'
),
insertSpaceAfterKeywordsInControlFlowStatements: config.get<boolean>(
'insertSpaceAfterKeywordsInControlFlowStatements'
),
insertSpaceAfterFunctionKeywordForAnonymousFunctions: config.get<boolean>(
'insertSpaceAfterFunctionKeywordForAnonymousFunctions'
),
insertSpaceBeforeFunctionParenthesis: config.get<boolean>(
'insertSpaceBeforeFunctionParenthesis'
),
insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: config.get<boolean>(
'insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis'
),
insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: config.get<boolean>(
'insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets'
),
insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: config.get<boolean>(
'insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces'
),
insertSpaceAfterOpeningAndBeforeClosingEmptyBraces: config.get<boolean>(
'insertSpaceAfterOpeningAndBeforeClosingEmptyBraces'
),
insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: config.get<boolean>(
'insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces'
),
insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces: config.get<boolean>(
'insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces'
),
insertSpaceAfterTypeAssertion: config.get<boolean>('insertSpaceAfterTypeAssertion'),
placeOpenBraceOnNewLineForFunctions: config.get<boolean>('placeOpenBraceOnNewLineForFunctions'),
placeOpenBraceOnNewLineForControlBlocks: config.get<boolean>(
'placeOpenBraceOnNewLineForControlBlocks'
),
semicolons: config.get<ts.server.protocol.SemicolonPreference>('semicolons'),
};
}
Loading