Skip to content

Commit

Permalink
feat: Introduce InlayHints
Browse files Browse the repository at this point in the history
  • Loading branch information
chadhietala committed Mar 1, 2023
1 parent adf49dd commit 943d596
Show file tree
Hide file tree
Showing 7 changed files with 378 additions and 26 deletions.
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",
"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,
}
);

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

0 comments on commit 943d596

Please sign in to comment.