From 3a7e6d04a5bfa9462fbe00a314d11cd5bcba2fe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20=C5=BBak?= Date: Fri, 12 Nov 2021 19:00:18 +0100 Subject: [PATCH] feat: hover provider for intl addon (translations) (#340) * translations hover for intl * move `focusPath` logic out of addon into hover providers entrypoint * test for translation autocomplete basing on translation text instead of key --- .../core/intl-completion-provider.ts | 4 +- .../core/intl-hover-provider.ts | 41 ++++++ src/hover-provider/entry.ts | 66 +++++++++- src/utils/addon-api.ts | 2 +- src/utils/builtin-addons-initializer.ts | 6 +- .../bultin-addons/core/intl-providers-test.ts | 123 ++++++++++++++++-- test/test_helpers/integration-helpers.ts | 2 +- 7 files changed, 225 insertions(+), 19 deletions(-) create mode 100644 src/builtin-addons/core/intl-hover-provider.ts diff --git a/src/builtin-addons/core/intl-completion-provider.ts b/src/builtin-addons/core/intl-completion-provider.ts index 63189289..b232b743 100644 --- a/src/builtin-addons/core/intl-completion-provider.ts +++ b/src/builtin-addons/core/intl-completion-provider.ts @@ -54,7 +54,7 @@ export default class IntlCompletionProvider { end: endPosition, }, }, - detail: detail, + documentation: detail, }); } @@ -74,7 +74,7 @@ export default class IntlCompletionProvider { }, }, filterText: t.text + ' ' + t.locale, - detail: detail, + documentation: detail, }); }); }); diff --git a/src/builtin-addons/core/intl-hover-provider.ts b/src/builtin-addons/core/intl-hover-provider.ts new file mode 100644 index 00000000..75339c90 --- /dev/null +++ b/src/builtin-addons/core/intl-hover-provider.ts @@ -0,0 +1,41 @@ +import { ASTv1 } from '@glimmer/syntax'; +import { Hover } from 'vscode-languageserver'; +import { Server } from '../..'; +import { nodeLoc } from '../../glimmer-utils'; +import { HoverFunctionParams } from '../../utils/addon-api'; +import { isLocalizationHelperTranslataionName } from '../../utils/ast-helpers'; +import { getTranslations } from './intl-utils'; + +export default class IntlHoverProvider { + server: Server; + onInit(server: Server) { + this.server = server; + } + + async onHover(root: string, params: HoverFunctionParams): Promise { + const { results, focusPath, type } = params; + + if (isLocalizationHelperTranslataionName(focusPath, type)) { + const node = focusPath.node as ASTv1.StringLiteral; + const key = node.value; + const translations = await getTranslations(root, this.server); + const location = nodeLoc(node); + + Object.keys(translations).forEach((tr) => { + if (tr === key) { + const detail = translations[tr].map((t) => `${t.locale} : ${t.text}`).join('\n'); + + results.push({ + contents: { kind: 'plaintext', value: detail }, + range: { + start: { line: location.start.line - 1, character: location.start.column }, + end: { line: location.end.line - 1, character: location.end.column }, + }, + }); + } + }); + } + + return results; + } +} diff --git a/src/hover-provider/entry.ts b/src/hover-provider/entry.ts index 94f9b71f..230cb44a 100644 --- a/src/hover-provider/entry.ts +++ b/src/hover-provider/entry.ts @@ -1,7 +1,12 @@ -import { Hover, HoverParams } from 'vscode-languageserver/node'; +import { preprocess } from '@glimmer/syntax'; +import { parseScriptFile } from 'ember-meta-explorer'; +import { Hover, HoverParams, Position, TextDocumentIdentifier } from 'vscode-languageserver'; +import { toPosition } from '../estree-utils'; +import ASTPath from '../glimmer-utils'; import Server from '../server'; +import { isScriptPath, isTemplatePath } from '../utils/layout-helpers'; +import { logDebugInfo } from '../utils/logger'; import { queryELSAddonsAPIChain } from './../utils/addon-api'; - export class HoverProvider { constructor(private server: Server) {} async provideHover({ textDocument, position }: HoverParams): Promise { @@ -11,17 +16,72 @@ export class HoverProvider { return null; } - const addonResults = await queryELSAddonsAPIChain(project.providers.hoverProviders, project.root, { + const { focusPath, type } = this.getFocusPath(textDocument, position); + + if (!focusPath) { + return null; + } + + const internalResults = await queryELSAddonsAPIChain(project.builtinProviders.hoverProviders, project.root, { textDocument, + focusPath, + type, position, results: [], server: this.server, }); + const addonResults = await queryELSAddonsAPIChain(project.providers.hoverProviders, project.root, { + textDocument, + focusPath, + type, + position, + results: internalResults, + server: this.server, + }); + if (addonResults.length) { return addonResults[0]; } return null; } + + getFocusPath(textDocument: TextDocumentIdentifier, position: Position) { + const project = this.server.projectRoots.projectForUri(textDocument.uri); + + if (!project) { + return {}; + } + + const document = this.server.documents.get(textDocument.uri); + const content = document?.getText(); + + if (!content) { + return {}; + } + + let ast = null; + let type: 'script' | 'template'; + + try { + if (isScriptPath(textDocument.uri)) { + ast = parseScriptFile(content); + type = 'script'; + } else if (isTemplatePath(textDocument.uri)) { + ast = preprocess(content); + type = 'template'; + } else { + return {}; + } + } catch (e) { + logDebugInfo('error', e); + + return {}; + } + + const focusPath = ASTPath.toPosition(ast, toPosition(position), content); + + return { focusPath, type }; + } } diff --git a/src/utils/addon-api.ts b/src/utils/addon-api.ts index 26013e72..d3d4007d 100644 --- a/src/utils/addon-api.ts +++ b/src/utils/addon-api.ts @@ -31,7 +31,7 @@ export interface ReferenceFunctionParams extends BaseAPIParams { results: Location[]; } -export interface HoverFunctionParams extends BaseAPIParams { +export interface HoverFunctionParams extends ExtendedAPIParams { results: Hover[]; } export interface CompletionFunctionParams extends ExtendedAPIParams { diff --git a/src/utils/builtin-addons-initializer.ts b/src/utils/builtin-addons-initializer.ts index f04f84b2..40c804cc 100644 --- a/src/utils/builtin-addons-initializer.ts +++ b/src/utils/builtin-addons-initializer.ts @@ -9,6 +9,7 @@ import IntlCompletionProvider from '../builtin-addons/core/intl-completion-provi import { AddonMeta, ProjectProviders } from './addon-api'; import { logInfo } from './logger'; import IntlDefinitionProvider from '../builtin-addons/core/intl-definition-provider'; +import IntlHoverProvider from '../builtin-addons/core/intl-hover-provider'; export function initBuiltinProviders(addonsMeta: AddonMeta[]): ProjectProviders { const scriptDefinition = new CoreScriptDefinitionProvider(); @@ -20,6 +21,7 @@ export function initBuiltinProviders(addonsMeta: AddonMeta[]): ProjectProviders const templateLintCommentsCodeAction = new TemplateLintCommentsCodeAction(); const typedTemplatesCodeAction = new TypedTemplatesCodeAction(); const intlDefinition = new IntlDefinitionProvider(); + const intlHover = new IntlHoverProvider(); const definitionProviders = [ scriptDefinition.onDefinition.bind(scriptDefinition), @@ -41,8 +43,10 @@ export function initBuiltinProviders(addonsMeta: AddonMeta[]): ProjectProviders templateDefinition.onInit.bind(templateDefinition), scriptDefinition.onInit.bind(scriptDefinition), intlDefinition.onInit.bind(intlDefinition), + intlHover.onInit.bind(intlHover), ]; const completionProviders = [scriptCompletion.onComplete.bind(scriptCompletion), templateCompletion.onComplete.bind(templateCompletion)]; + const hoverProviders = [intlHover.onHover.bind(intlHover)]; if (!addonsMeta.find((addon) => addon.name == 'els-intl-addon')) { const intlCompletion = new IntlCompletionProvider(); @@ -57,7 +61,7 @@ export function initBuiltinProviders(addonsMeta: AddonMeta[]): ProjectProviders definitionProviders, referencesProviders, codeActionProviders, - hoverProviders: [], + hoverProviders, initFunctions, info: [], addonsMeta: [], diff --git a/test/bultin-addons/core/intl-providers-test.ts b/test/bultin-addons/core/intl-providers-test.ts index e68fe497..f170cc29 100644 --- a/test/bultin-addons/core/intl-providers-test.ts +++ b/test/bultin-addons/core/intl-providers-test.ts @@ -1,5 +1,5 @@ import { MessageConnection } from 'vscode-jsonrpc'; -import { CompletionRequest, DefinitionRequest } from 'vscode-languageserver-protocol'; +import { CompletionRequest, DefinitionRequest, HoverRequest } from 'vscode-languageserver-protocol'; import { createServer, ServerBucket, getResult, makeProject } from '../../test_helpers/public-integration-helpers'; const testCaseAsyncFsOptions = [false, true]; @@ -166,7 +166,7 @@ for (const asyncFsEnabled of testCaseAsyncFsOptions) { ).response ).toEqual([ { - detail: 'en-us : text 1\npl-pl : text 1 in polish', + documentation: 'en-us : text 1\npl-pl : text 1 in polish', kind: 12, label: 'rootFileTranslation', textEdit: { @@ -206,7 +206,7 @@ for (const asyncFsEnabled of testCaseAsyncFsOptions) { ).response ).toEqual([ { - detail: 'en-us : text 1\npl-pl : text 1 in polish', + documentation: 'en-us : text 1\npl-pl : text 1 in polish', kind: 12, label: 'rootFileTranslation', textEdit: { @@ -246,7 +246,7 @@ for (const asyncFsEnabled of testCaseAsyncFsOptions) { ).response ).toEqual([ { - detail: 'en-us : text 2', + documentation: 'en-us : text 2', kind: 12, label: 'subFolderTranslation.subTranslation', textEdit: { @@ -264,7 +264,7 @@ for (const asyncFsEnabled of testCaseAsyncFsOptions) { }, }, { - detail: 'en-us : another text', + documentation: 'en-us : another text', kind: 12, label: 'subFolderTranslation.anotherTranslation', textEdit: { @@ -304,7 +304,7 @@ for (const asyncFsEnabled of testCaseAsyncFsOptions) { ).response ).toEqual([ { - detail: 'en-us : another text', + documentation: 'en-us : another text', kind: 12, label: 'subFolderTranslation.anotherTranslation', textEdit: { @@ -344,7 +344,7 @@ for (const asyncFsEnabled of testCaseAsyncFsOptions) { ).response ).toEqual([ { - detail: 'en-us : text 2', + documentation: 'en-us : text 2', kind: 12, label: 'subFolderTranslation.subTranslation', textEdit: { @@ -362,7 +362,7 @@ for (const asyncFsEnabled of testCaseAsyncFsOptions) { }, }, { - detail: 'en-us : another text', + documentation: 'en-us : another text', kind: 12, label: 'subFolderTranslation.anotherTranslation', textEdit: { @@ -381,6 +381,47 @@ for (const asyncFsEnabled of testCaseAsyncFsOptions) { }, ]); }); + it('should autocomplete translation base on translation text', async () => { + expect( + ( + await getResult( + CompletionRequest.method, + connection, + { + app: { + components: { + 'test.hbs': `{{t "polish" }}`, + }, + }, + translations, + }, + 'app/components/test.hbs', + { line: 0, character: 8 } + ) + ).response + ).toEqual([ + { + documentation: 'en-us : text 1\npl-pl : text 1 in polish', + filterText: 'text 1 in polish pl-pl', + kind: 12, + label: 'text 1 in polish', + textEdit: { + newText: 'rootFileTranslation', + + range: { + end: { + character: 5, + line: 0, + }, + start: { + character: 5, + line: 0, + }, + }, + }, + }, + ]); + }); }); describe('provide completion - YAML', () => { @@ -404,7 +445,7 @@ for (const asyncFsEnabled of testCaseAsyncFsOptions) { ).response ).toEqual([ { - detail: 'en-us : text 1', + documentation: 'en-us : text 1', kind: 12, label: 'rootFileTranslation', textEdit: { @@ -444,7 +485,7 @@ for (const asyncFsEnabled of testCaseAsyncFsOptions) { ).response ).toEqual([ { - detail: 'en-us : text 2', + documentation: 'en-us : text 2', kind: 12, label: 'subFolderTranslation.subTranslation', textEdit: { @@ -462,7 +503,7 @@ for (const asyncFsEnabled of testCaseAsyncFsOptions) { }, }, { - detail: 'en-us : another text', + documentation: 'en-us : another text', kind: 12, label: 'subFolderTranslation.anotherTranslation', textEdit: { @@ -628,5 +669,65 @@ for (const asyncFsEnabled of testCaseAsyncFsOptions) { ]); }); }); + + describe('provide hover', () => { + it('should provide translation hover in handlebars', async () => { + expect( + ((await getResult( + HoverRequest.method, + connection, + { + app: { + components: { + 'test.hbs': '{{t "rootFileTranslation" }}', + }, + }, + translations, + }, + 'app/components/test.hbs', + { line: 0, character: 20 } + )) as any).response + ).toEqual({ + contents: { + kind: 'plaintext', + value: 'en-us : text 1\npl-pl : text 1 in polish', + }, + + range: { + start: { line: 0, character: 4 }, + end: { line: 0, character: 25 }, + }, + }); + }); + + it('should provide translation hover in js', async () => { + expect( + ((await getResult( + HoverRequest.method, + connection, + { + app: { + components: { + 'test.js': 'export default class Foo extends Bar { text = this.intl.t("rootFileTranslation"); }', + }, + }, + translations, + }, + 'app/components/test.js', + { line: 0, character: 70 } + )) as any).response + ).toEqual({ + contents: { + kind: 'plaintext', + value: 'en-us : text 1\npl-pl : text 1 in polish', + }, + + range: { + start: { line: 0, character: 58 }, + end: { line: 0, character: 79 }, + }, + }); + }); + }); }); } diff --git a/test/test_helpers/integration-helpers.ts b/test/test_helpers/integration-helpers.ts index ad28a43f..bcf1f70f 100644 --- a/test/test_helpers/integration-helpers.ts +++ b/test/test_helpers/integration-helpers.ts @@ -405,7 +405,7 @@ export async function getResult( files, fileToInspect: string, position: { line: number; character: number }, - projectName: string[] + projectName?: string[] ): Promise[]>; export async function getResult( reqType: typeof CompletionRequest.method,