diff --git a/.vscode/launch.json b/.vscode/launch.json index 7c03cce50..dca73dbc3 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,8 @@ "name": "Launch Client", "runtimeExecutable": "${execPath}", "args": [ - "--extensionDevelopmentPath=${workspaceRoot}/packages/vscode-ui5-language-assistant" + "--extensionDevelopmentPath=${workspaceRoot}/packages/vscode-ui5-language-assistant", + "--disable-extensions" ], "outFiles": [ "${workspaceRoot}/packages/vscode-ui5-language-assistant/lib/src/**/*.js" diff --git a/packages/language-server/api.d.ts b/packages/language-server/api.d.ts index 87ec846c9..6aed2f0d5 100644 --- a/packages/language-server/api.d.ts +++ b/packages/language-server/api.d.ts @@ -2,8 +2,8 @@ * Absolute path to the server's "main" module * This is useful when launching the server in a separate process (e.g via spawn). */ -import { LogLevel } from "@vscode-logging/types"; +import { LogLevel } from "@vscode-logging/types"; export declare const SERVER_PATH: string; export type ServerInitializationOptions = { @@ -25,6 +25,13 @@ export type ServerInitializationOptions = { logLevel?: LogLevel; }; +export function getNodeName( + text: string, + offsetAt: number, + cachePath?: string, + framework?: string, + ui5Version?: string +): Promise; export type FetchResponse = { ok: boolean; status: number; diff --git a/packages/language-server/src/api.ts b/packages/language-server/src/api.ts index cfc5daf8d..e5576d85f 100644 --- a/packages/language-server/src/api.ts +++ b/packages/language-server/src/api.ts @@ -1,5 +1,6 @@ import { resolve } from "path"; import { existsSync } from "fs"; +import { getNodeDetail, getUI5NodeName } from "./documentation"; // for use in productive flows const bundledPath = resolve(__dirname, "..", "..", "dist", "server.js"); @@ -19,3 +20,25 @@ export const SERVER_PATH: string = isDevelopmentRun ? /* istanbul ignore else - no tests (yet?) on bundled artifacts */ sourcesPath : bundledPath; + +export async function getNodeName( + text: string, + offsetAt: number, + cachePath?: string | undefined, + framework?: string | undefined, + ui5Version?: string | undefined +): Promise { + const ui5Node = await getUI5NodeName( + offsetAt, + text, + undefined, + cachePath, + framework, + ui5Version + ); + return ui5Node + ? ui5Node.kind !== "UI5Class" + ? `${ui5Node.parent?.library}.${ui5Node.parent?.name}` + : getNodeDetail(ui5Node) + : undefined; +} diff --git a/packages/language-server/src/documentation.ts b/packages/language-server/src/documentation.ts index 2f4a8894d..5b39b3380 100644 --- a/packages/language-server/src/documentation.ts +++ b/packages/language-server/src/documentation.ts @@ -17,6 +17,11 @@ import { getLink, } from "@ui5-language-assistant/logic-utils"; import { GENERATED_LIBRARY } from "@ui5-language-assistant/semantic-model"; +import { DocumentCstNode, parse } from "@xml-tools/parser"; +import { buildAst } from "@xml-tools/ast"; +import { astPositionAtOffset } from "@xml-tools/ast-position"; +import { findUI5HoverNodeAtOffset } from "@ui5-language-assistant/xml-views-tooltip"; +import { getSemanticModel } from "./ui5-model"; export function getNodeDocumentation( node: BaseUI5Node, @@ -108,3 +113,30 @@ export function getNodeDetail(node: BaseUI5Node): string { return node.name; } } + +export async function getUI5NodeName( + offsetAt: number, + text: string, + model?: UI5SemanticModel, + cachePath?: string, + framework?: string, + ui5Version?: string +): Promise { + const documentText = text; + const { cst, tokenVector } = parse(documentText); + const ast = buildAst(cst as DocumentCstNode, tokenVector); + const offset = offsetAt; + const astPosition = astPositionAtOffset(ast, offset); + if (!model) { + model = await fetchModel(cachePath, framework, ui5Version); + } + if (astPosition !== undefined) { + return findUI5HoverNodeAtOffset(astPosition, model); + } + return undefined; +} + +async function fetchModel(cachePath, framework, ui5Version) { + const ui5Model = await getSemanticModel(cachePath, framework, ui5Version); + return ui5Model; +} diff --git a/packages/language-server/src/hover.ts b/packages/language-server/src/hover.ts index 04b92f80c..04ba66a8f 100644 --- a/packages/language-server/src/hover.ts +++ b/packages/language-server/src/hover.ts @@ -5,33 +5,30 @@ import { MarkupContent, MarkupKind, } from "vscode-languageserver"; -import { parse, DocumentCstNode } from "@xml-tools/parser"; -import { buildAst } from "@xml-tools/ast"; -import { astPositionAtOffset } from "@xml-tools/ast-position"; import { UI5SemanticModel, BaseUI5Node, } from "@ui5-language-assistant/semantic-model-types"; -import { findUI5HoverNodeAtOffset } from "@ui5-language-assistant/xml-views-tooltip"; -import { getNodeDocumentation, getNodeDetail } from "./documentation"; +import { + getNodeDocumentation, + getNodeDetail, + getUI5NodeName, +} from "./documentation"; import { track } from "./swa"; -export function getHoverResponse( +export async function getHoverResponse( model: UI5SemanticModel, textDocumentPosition: TextDocumentPositionParams, document: TextDocument -): Hover | undefined { - const documentText = document.getText(); - const { cst, tokenVector } = parse(documentText); - const ast = buildAst(cst as DocumentCstNode, tokenVector); - const offset = document.offsetAt(textDocumentPosition.position); - const astPosition = astPositionAtOffset(ast, offset); - if (astPosition !== undefined) { - const ui5Node = findUI5HoverNodeAtOffset(astPosition, model); - if (ui5Node !== undefined) { - track("XML_UI5_DOC_HOVER", ui5Node.kind); - return transformToLspHover(ui5Node, model); - } +): Promise { + const ui5Node = await getUI5NodeName( + document.offsetAt(textDocumentPosition.position), + document.getText(), + model + ); + if (ui5Node !== undefined) { + track("XML_UI5_DOC_HOVER", ui5Node.kind); + return transformToLspHover(ui5Node, model); } return undefined; diff --git a/packages/language-server/src/server.ts b/packages/language-server/src/server.ts index 11386847d..70b636d17 100644 --- a/packages/language-server/src/server.ts +++ b/packages/language-server/src/server.ts @@ -94,6 +94,8 @@ connection.onInitialize((params: InitializeParams) => { commands.QUICK_FIX_STABLE_ID_FILE_ERRORS.name, ], }, + referencesProvider: false, + definitionProvider: true, }, }; }); @@ -128,6 +130,7 @@ connection.onCompletion( minUI5Version ); connection.sendNotification("UI5LanguageAssistant/ui5Model", { + cachePath: initializationOptions?.modelCachePath, url: getCDNBaseUrl(framework, model.version), framework, version: model.version, @@ -172,6 +175,7 @@ connection.onHover( minUI5Version ); connection.sendNotification("UI5LanguageAssistant/ui5Model", { + cachePath: initializationOptions?.modelCachePath, url: getCDNBaseUrl(framework, ui5Model.version), framework, version: ui5Model.version, @@ -226,6 +230,8 @@ documents.onDidChangeContent(async (changeEvent) => { minUI5Version ); connection.sendNotification("UI5LanguageAssistant/ui5Model", { + cachePath: initializationOptions?.modelCachePath, + url: getCDNBaseUrl(framework, ui5Model.version), framework, version: ui5Model.version, @@ -258,6 +264,8 @@ connection.onCodeAction(async (params) => { minUI5Version ); connection.sendNotification("UI5LanguageAssistant/ui5Model", { + cachePath: initializationOptions?.modelCachePath, + url: getCDNBaseUrl(framework, ui5Model.version), framework, version: ui5Model.version, diff --git a/packages/language-server/test/documentation-spec.ts b/packages/language-server/test/documentation-spec.ts index ae9a548db..fb505a263 100644 --- a/packages/language-server/test/documentation-spec.ts +++ b/packages/language-server/test/documentation-spec.ts @@ -5,7 +5,16 @@ import { } from "@ui5-language-assistant/test-utils"; import { generate } from "@ui5-language-assistant/semantic-model"; import { UI5SemanticModel } from "@ui5-language-assistant/semantic-model-types"; -import { getNodeDocumentation } from "../src/documentation"; +import { + getNodeDetail, + getNodeDocumentation, + getUI5NodeName, +} from "../src/documentation"; +import { dir as tempDir } from "tmp-promise"; + +import { Position } from "vscode-languageserver"; +import { TextDocument } from "vscode-languageserver-textdocument"; +import { getSemanticModel } from "../src/ui5-model"; describe("The @ui5-language-assistant/language-server function", () => { let ui5SemanticModel: UI5SemanticModel; @@ -89,5 +98,83 @@ describe("The @ui5-language-assistant/language-server fun const result = getNodeDocumentation(ui5Enum, ui5SemanticModel); expect(result.value).to.include("Experimental."); }); + + it("get the UI5 node name with a model", async () => { + const xmlSnippet = ` + + + + + + `; + const { document, position } = getXmlSnippet(xmlSnippet); + + const result = await getUI5NodeName( + document.offsetAt(position), + document.getText(), + ui5SemanticModel + ); + if (result) { + expect(getNodeDetail(result)).to.equal("sap.m.Input"); + } else { + expect(result).to.equal("sap.m.Input"); + } + }); + + it("get the UI5 node name without a model", async () => { + const xmlSnippet = ` + + + + + + `; + + const cachePath = await tempDir(); + const ui5Model = await getSemanticModel( + cachePath.path, + "SAPUI5", + undefined, + true + ); + + const { document, position } = getXmlSnippet(xmlSnippet); + + const result = await getUI5NodeName( + document.offsetAt(position), + document.getText(), + undefined, + cachePath.path, + "SAPUI5", + ui5Model.version + ); + if (result) expect(getNodeDetail(result)).to.equal("sap.m.Input"); + else { + expect(result).to.equal("sap.m.Input"); + } + }); }); + + function getXmlSnippet( + xmlSnippet: string + ): { document: TextDocument; position: Position } { + const xmlText = xmlSnippet.replace("⇶", ""); + const offset = xmlSnippet.indexOf("Input"); + const document: TextDocument = createTextDocument("xml", xmlText); + const position: Position = document.positionAt(offset); + return { document, position }; + } + + function createTextDocument( + languageId: string, + content: string + ): TextDocument { + return TextDocument.create("uri", languageId, 0, content); + } }); diff --git a/packages/language-server/test/hover-spec.ts b/packages/language-server/test/hover-spec.ts index 6f9d3d8ec..447613cf6 100644 --- a/packages/language-server/test/hover-spec.ts +++ b/packages/language-server/test/hover-spec.ts @@ -28,7 +28,7 @@ describe("the UI5 language assistant Hover Tooltip Service", () => { }); context("hover on attribute key", () => { - it("will get hover content UI5 property", () => { + it("will get hover content UI5 property", async () => { const xmlSnippet = ` @@ -36,7 +36,7 @@ describe("the UI5 language assistant Hover Tooltip Service", () => { `; - const response = getHoverItem(xmlSnippet, ui5SemanticModel); + const response = await getHoverItem(xmlSnippet, ui5SemanticModel); expectExists(response, "Hover item"); assertMarkup(response.contents); expect(response.contents.value).to.include( @@ -44,13 +44,13 @@ describe("the UI5 language assistant Hover Tooltip Service", () => { ); }); - it("will get hover content UI5 association", () => { + it("will get hover content UI5 association", async () => { const xmlSnippet = ` `; - const response = getHoverItem(xmlSnippet, ui5SemanticModel); + const response = await getHoverItem(xmlSnippet, ui5SemanticModel); expectExists(response, "Hover item"); assertMarkup(response.contents); expect(response.contents.value).to.include( @@ -58,12 +58,12 @@ describe("the UI5 language assistant Hover Tooltip Service", () => { ); }); - it("will get hover content UI5 event", () => { + it("will get hover content UI5 event", async () => { const xmlSnippet = ` `; - const response = getHoverItem(xmlSnippet, ui5SemanticModel); + const response = await getHoverItem(xmlSnippet, ui5SemanticModel); expectExists(response, "Hover item"); assertMarkup(response.contents); expect(response.contents.value).to.include( @@ -71,7 +71,7 @@ describe("the UI5 language assistant Hover Tooltip Service", () => { ); }); - it("will get hover content UI5 property - incorrect property", () => { + it("will get hover content UI5 property - incorrect property", async () => { const xmlSnippet = ` @@ -79,13 +79,13 @@ describe("the UI5 language assistant Hover Tooltip Service", () => { `; - const response = getHoverItem(xmlSnippet, ui5SemanticModel); + const response = await getHoverItem(xmlSnippet, ui5SemanticModel); expect(response).to.not.exist; }); }); - context("hover on attribute value", () => { - it("will get hover content UI5 enum", () => { + context("hover on attribute value", async () => { + it("will get hover content UI5 enum", async () => { const xmlSnippet = ` @@ -93,7 +93,7 @@ describe("the UI5 language assistant Hover Tooltip Service", () => { `; - const response = getHoverItem(xmlSnippet, ui5SemanticModel); + const response = await getHoverItem(xmlSnippet, ui5SemanticModel); expectExists(response, "Hover item"); assertMarkup(response.contents); expect(response.contents.value).to.include( @@ -101,7 +101,7 @@ describe("the UI5 language assistant Hover Tooltip Service", () => { ); }); - it("will get hover content UI5 namespace in xmlns attribute", () => { + it("will get hover content UI5 namespace in xmlns attribute", async () => { const xmlSnippet = ` @@ -109,7 +109,7 @@ describe("the UI5 language assistant Hover Tooltip Service", () => { `; - const response = getHoverItem(xmlSnippet, ui5SemanticModel); + const response = await getHoverItem(xmlSnippet, ui5SemanticModel); expectExists(response, "Hover item"); assertMarkup(response.contents); expect(response.contents.value).to.include( @@ -119,7 +119,7 @@ describe("the UI5 language assistant Hover Tooltip Service", () => { }); context("hover on element open tag name", () => { - it("will get hover content UI5 class", () => { + it("will get hover content UI5 class", async () => { const xmlSnippet = ` @@ -127,7 +127,7 @@ describe("the UI5 language assistant Hover Tooltip Service", () => { `; - const response = getHoverItem(xmlSnippet, ui5SemanticModel); + const response = await getHoverItem(xmlSnippet, ui5SemanticModel); expectExists(response, "Hover item"); assertMarkup(response.contents); expect(response.contents.value).to.include( @@ -135,7 +135,7 @@ describe("the UI5 language assistant Hover Tooltip Service", () => { ); }); - it("will get hover content UI5 Aggregation", () => { + it("will get hover content UI5 Aggregation", async () => { const xmlSnippet = ` @@ -143,7 +143,7 @@ describe("the UI5 language assistant Hover Tooltip Service", () => { `; - const response = getHoverItem(xmlSnippet, ui5SemanticModel); + const response = await getHoverItem(xmlSnippet, ui5SemanticModel); expectExists(response, "Hover item"); assertMarkup(response.contents); expect(response.contents.value).to.include("Child Controls of the view"); @@ -151,7 +151,7 @@ describe("the UI5 language assistant Hover Tooltip Service", () => { }); context("hover on element close tag name", () => { - it("will get hover content UI5 class", () => { + it("will get hover content UI5 class", async () => { const xmlSnippet = ` @@ -159,7 +159,7 @@ describe("the UI5 language assistant Hover Tooltip Service", () => { `; - const response = getHoverItem(xmlSnippet, ui5SemanticModel); + const response = await getHoverItem(xmlSnippet, ui5SemanticModel); expectExists(response, "Hover item"); assertMarkup(response.contents); expect(response.contents.value).to.include( @@ -167,7 +167,7 @@ describe("the UI5 language assistant Hover Tooltip Service", () => { ); }); - it("will get hover content UI5 Aggregation", () => { + it("will get hover content UI5 Aggregation", async () => { const xmlSnippet = ` @@ -175,20 +175,20 @@ describe("the UI5 language assistant Hover Tooltip Service", () => { `; - const response = getHoverItem(xmlSnippet, ui5SemanticModel); + const response = await getHoverItem(xmlSnippet, ui5SemanticModel); expectExists(response, "Hover item"); assertMarkup(response.contents); expect(response.contents.value).to.include("Child Controls of the view"); }); }); - it("will get hover content - open tag is different from close tag", () => { + it("will get hover content - open tag is different from close tag", async () => { const xmlSnippet = ` `; - const response = getHoverItem(xmlSnippet, ui5SemanticModel); + const response = await getHoverItem(xmlSnippet, ui5SemanticModel); expectExists(response, "Hover item"); assertMarkup(response.contents); expect(response.contents.value).to.include( @@ -196,7 +196,7 @@ describe("the UI5 language assistant Hover Tooltip Service", () => { ); }); - it("will get undefined hover content", () => { + it("will get undefined hover content", async () => { const xmlSnippet = ` @@ -204,15 +204,15 @@ describe("the UI5 language assistant Hover Tooltip Service", () => { `; - const response = getHoverItem(xmlSnippet, ui5SemanticModel); + const response = await getHoverItem(xmlSnippet, ui5SemanticModel); expect(response).to.not.exist; }); }); -export function getHoverItem( +export async function getHoverItem( xmlSnippet: string, ui5SemanticModel: UI5SemanticModel -): Hover | undefined { +): Promise { const { document, position } = getXmlSnippet(xmlSnippet); const uri: TextDocumentIdentifier = { uri: "uri" }; const textDocPositionParams: TextDocumentPositionParams = { @@ -220,7 +220,7 @@ export function getHoverItem( position: position, }; - const hoverItem = getHoverResponse( + const hoverItem = await getHoverResponse( ui5SemanticModel, textDocPositionParams, document diff --git a/packages/settings/api.d.ts b/packages/settings/api.d.ts index a5b06bdac..c92949139 100644 --- a/packages/settings/api.d.ts +++ b/packages/settings/api.d.ts @@ -1,5 +1,8 @@ import Thenable from "vscode-languageserver"; -export type Settings = CodeAssistSettings & TraceSettings & LoggingSettings; +export type Settings = CodeAssistSettings & + TraceSettings & + LoggingSettings & + API_Reference; export interface CodeAssistSettings { codeAssist: { @@ -14,12 +17,25 @@ export interface TraceSettings { }; } +export interface API_Reference { + view: { + API_Reference: keyof IValidApiReferenceValues; + }; +} + export interface IValidTraceServerValues { off: true; messages: true; verbose: true; } +export interface IValidApiReferenceValues { + editor: true; + browser: true; +} + +export const validViewApiReferenceValues: IValidApiReferenceValues; + export const validTraceServerValues: IValidTraceServerValues; export interface LoggingSettings { diff --git a/packages/settings/src/api.ts b/packages/settings/src/api.ts index 738697e90..b49487c46 100644 --- a/packages/settings/src/api.ts +++ b/packages/settings/src/api.ts @@ -14,6 +14,11 @@ export const validTraceServerValues = { verbose: true as const, }; +export const validViewApiReferenceValues = { + editor: true as const, + browser: true as const, +}; + export const validLoggingLevelValues = { off: true as const, fatal: true as const, diff --git a/packages/settings/src/settings.ts b/packages/settings/src/settings.ts index 0df5b54fc..2a9bfbd28 100644 --- a/packages/settings/src/settings.ts +++ b/packages/settings/src/settings.ts @@ -7,6 +7,7 @@ const defaultSettings: Settings = { codeAssist: { deprecated: false, experimental: false }, trace: { server: "off" }, logging: { level: "error" }, + view: { API_Reference: "editor" }, }; deepFreezeStrict(defaultSettings); diff --git a/packages/settings/test/settings-spec.ts b/packages/settings/test/settings-spec.ts index cc238a1b5..9bd2fc55c 100644 --- a/packages/settings/test/settings-spec.ts +++ b/packages/settings/test/settings-spec.ts @@ -41,6 +41,7 @@ context("settings utilities", () => { codeAssist: { deprecated: true, experimental: false }, trace: { server: "off" as const }, logging: { level: "off" as const }, + view: { API_Reference: "editor" as const }, }; setGlobalSettings(globalSettings); const docSettings = await getSettingsForDocument("doc1"); @@ -52,6 +53,7 @@ context("settings utilities", () => { codeAssist: { deprecated: true, experimental: false }, trace: { server: "off" as const }, logging: { level: "off" as const }, + view: { API_Reference: "editor" as const }, }; setGlobalSettings(globalSettings); const docSettings = await getSettingsForDocument("doc1"); @@ -65,6 +67,7 @@ context("settings utilities", () => { codeAssist: { deprecated: true, experimental: true }, trace: { server: "off" as const }, logging: { level: "off" as const }, + view: { API_Reference: "editor" as const }, }; setSettingsForDocument("doc1", Promise.resolve(docSettings)); const result = await getSettingsForDocument("doc1"); @@ -80,6 +83,7 @@ context("settings utilities", () => { codeAssist: { deprecated: true, experimental: true }, trace: { server: "off" as const }, logging: { level: "off" as const }, + view: { API_Reference: "editor" as const }, }) ); expect(hasSettingsForDocument("doc1")).to.be.true; @@ -95,6 +99,7 @@ context("settings utilities", () => { codeAssist: { deprecated: true, experimental: true }, trace: { server: "off" as const }, logging: { level: "off" as const }, + view: { API_Reference: "editor" as const }, }) ); expect(hasSettingsForDocument("doc1")).to.be.false; @@ -107,6 +112,7 @@ context("settings utilities", () => { codeAssist: { deprecated: true, experimental: true }, trace: { server: "off" as const }, logging: { level: "off" as const }, + view: { API_Reference: "editor" as const }, }; setSettingsForDocument("doc1", Promise.resolve(docSettings)); expect(await getSettingsForDocument("doc1")).to.deep.equal(docSettings); @@ -117,11 +123,13 @@ context("settings utilities", () => { codeAssist: { deprecated: true, experimental: true }, trace: { server: "off" as const }, logging: { level: "off" as const }, + view: { API_Reference: "editor" as const }, }; const docSettings2 = { codeAssist: { deprecated: true, experimental: false }, trace: { server: "off" as const }, logging: { level: "off" as const }, + view: { API_Reference: "editor" as const }, }; setSettingsForDocument("doc1", Promise.resolve(docSettings1)); setSettingsForDocument("doc1", Promise.resolve(docSettings2)); @@ -140,11 +148,13 @@ context("settings utilities", () => { codeAssist: { deprecated: true, experimental: true }, trace: { server: "off" as const }, logging: { level: "off" as const }, + view: { API_Reference: "editor" as const }, }; const docSettings2 = { codeAssist: { deprecated: true, experimental: false }, trace: { server: "off" as const }, logging: { level: "off" as const }, + view: { API_Reference: "editor" as const }, }; setSettingsForDocument("doc1", Promise.resolve(docSettings1)); setSettingsForDocument("doc2", Promise.resolve(docSettings2)); @@ -169,6 +179,7 @@ context("settings utilities", () => { codeAssist: { deprecated: true, experimental: true }, trace: { server: "off" as const }, logging: { level: "off" as const }, + view: { API_Reference: "editor" as const }, }; setSettingsForDocument("doc1", Promise.resolve(docSettings)); @@ -183,11 +194,13 @@ context("settings utilities", () => { codeAssist: { deprecated: true, experimental: true }, trace: { server: "off" as const }, logging: { level: "off" as const }, + view: { API_Reference: "editor" as const }, }; const docSettings2 = { codeAssist: { deprecated: true, experimental: false }, trace: { server: "off" as const }, logging: { level: "off" as const }, + view: { API_Reference: "editor" as const }, }; setSettingsForDocument("doc1", Promise.resolve(docSettings1)); setSettingsForDocument("doc2", Promise.resolve(docSettings2)); @@ -203,6 +216,7 @@ context("settings utilities", () => { codeAssist: { deprecated: true, experimental: true }, trace: { server: "off" as const }, logging: { level: "off" as const }, + view: { API_Reference: "editor" as const }, }; setGlobalSettings(globalSettings); expect(await getSettingsForDocument("doc1")).to.deep.equal( diff --git a/packages/vscode-ui5-language-assistant/README.md b/packages/vscode-ui5-language-assistant/README.md index ede258d6b..fd52bd667 100644 --- a/packages/vscode-ui5-language-assistant/README.md +++ b/packages/vscode-ui5-language-assistant/README.md @@ -124,6 +124,16 @@ The feature is available in the following: ![](https://raw.githubusercontent.com/SAP/ui5-language-assistant/master/packages/vscode-ui5-language-assistant/resources/readme/preview-manifest-json.gif) +### Quick navigation to API Reference + +Right click a tag in the XML file and use the View API reference shortcut to navigate directly to the API reference. Use the setting + +``` +"UI5LanguageAssistant.view.API_Reference": "browser" | "editor" +``` + +The default setting is editor which opens the API reference in a new editor to the side. + #### Description: Implemented using the UI5 [manifest.json schema](https://github.com/SAP/ui5-manifest/blob/master/schema.json). diff --git a/packages/vscode-ui5-language-assistant/package.json b/packages/vscode-ui5-language-assistant/package.json index 0651ee840..bade74fbd 100644 --- a/packages/vscode-ui5-language-assistant/package.json +++ b/packages/vscode-ui5-language-assistant/package.json @@ -20,6 +20,29 @@ "*" ], "contributes": { + "commands": [ + { + "command": "UI5LanguageAssistant.command.webView", + "title": "UI5-Language-Assistant: View API Reference" + } + ], + "menus": { + "editor/context": [ + { + "when": "resourceLangId == xml", + "command": "UI5LanguageAssistant.command.webView", + "group": "navigation" + } + ] + }, + "keybindings": [ + { + "command": "UI5LanguageAssistant.command.webView", + "key": "ctrl+alt+d", + "mac": "ctrl+opt+d", + "when": "resourceLangId == xml" + } + ], "jsonValidation": [ { "fileMatch": [ @@ -34,6 +57,15 @@ "type": "object", "title": "UI5 Language Assistant", "properties": { + "UI5LanguageAssistant.view.API_Reference": { + "type": "string", + "enum": [ + "browser", + "editor" + ], + "default": "editor", + "description": "How to open the API reference, in a browser window or webview inside the editor" + }, "UI5LanguageAssistant.logging.level": { "type": "string", "enum": [ diff --git a/packages/vscode-ui5-language-assistant/resources/logo_ui5.png b/packages/vscode-ui5-language-assistant/resources/logo_ui5.png new file mode 100644 index 000000000..627798ea1 Binary files /dev/null and b/packages/vscode-ui5-language-assistant/resources/logo_ui5.png differ diff --git a/packages/vscode-ui5-language-assistant/src/constants.ts b/packages/vscode-ui5-language-assistant/src/constants.ts index 799dfcb08..c5b42b462 100644 --- a/packages/vscode-ui5-language-assistant/src/constants.ts +++ b/packages/vscode-ui5-language-assistant/src/constants.ts @@ -1,2 +1,5 @@ export const COMMAND_OPEN_DEMOKIT = "UI5LanguageAssistant.command.openDemokit"; +export const COMMAND_OPEN_WEBVIEW = "UI5LanguageAssistant.command.webView"; export const LOGGING_LEVEL_CONFIG_PROP = "UI5LanguageAssistant.logging.level"; +export const VIEW_API_REF = "UI5LanguageAssistant.View.API_Reference"; +// export const REPLACE_GO_TO_DEFINITION = "editor.action.goToDeclaration"; diff --git a/packages/vscode-ui5-language-assistant/src/extension.ts b/packages/vscode-ui5-language-assistant/src/extension.ts index ccfc0ff52..d9dd9ab82 100644 --- a/packages/vscode-ui5-language-assistant/src/extension.ts +++ b/packages/vscode-ui5-language-assistant/src/extension.ts @@ -1,5 +1,6 @@ /* istanbul ignore file */ import { resolve } from "path"; +import { URL } from "url"; import { readFileSync } from "fs"; import { workspace, @@ -10,6 +11,8 @@ import { commands, env, Uri, + WebviewPanel, + ViewColumn, } from "vscode"; import { LanguageClient, @@ -21,27 +24,43 @@ import { LogLevel } from "@vscode-logging/types"; import { SERVER_PATH, ServerInitializationOptions, + getNodeName, } from "@ui5-language-assistant/language-server"; -import { COMMAND_OPEN_DEMOKIT, LOGGING_LEVEL_CONFIG_PROP } from "./constants"; - -type UI5Model = { url: string; framework: string; version: string }; +import { + COMMAND_OPEN_DEMOKIT, + COMMAND_OPEN_WEBVIEW, + LOGGING_LEVEL_CONFIG_PROP, + VIEW_API_REF, +} from "./constants"; + +type UI5Model = { + cachePath: string; + url: string; + framework: string; + version: string; +}; let client: LanguageClient; let statusBarItem: StatusBarItem; let currentModel: UI5Model | undefined; - +let currentPanel: WebviewPanel | undefined; +let extContext: ExtensionContext | undefined; export async function activate(context: ExtensionContext): Promise { // create the LanguageClient (+Server) client = createLanguageClient(context); - + extContext = context; // create the StatusBarItem which displays the used UI5 version statusBarItem = createStatusBarItem(context); - + createWebView(context); // show/hide and update the status bar client.start().then(() => { client.onNotification("UI5LanguageAssistant/ui5Model", (model: UI5Model) => updateCurrentModel(model) ); + + client.onNotification("UI5LanguageAssistant/ui5Definition", () => { + showWebView(); + }); }); window.onDidChangeActiveTextEditor(() => { updateCurrentModel(undefined); @@ -110,6 +129,12 @@ function createStatusBarItem(context: ExtensionContext): StatusBarItem { return statusBarItem; } +function createWebView(context: ExtensionContext) { + context.subscriptions.push( + commands.registerCommand(COMMAND_OPEN_WEBVIEW, showWebView) + ); +} + function updateCurrentModel(model: UI5Model | undefined) { currentModel = model; if (statusBarItem) { @@ -125,6 +150,101 @@ function updateCurrentModel(model: UI5Model | undefined) { } } +async function showWebView() { + const columnToShowIn = + window.activeTextEditor && window.activeTextEditor.viewColumn + ? window.activeTextEditor.viewColumn + 1 + : ViewColumn.One; + const apiRefUrl = currentModel?.url; + if (!apiRefUrl) + throw Error("UI5-Language-Assistant: Model not initialised, try again"); + const document = window.activeTextEditor?.document; + let name: string | undefined; + if ( + currentModel?.framework && + currentModel.version && + window.activeTextEditor?.selection.active && + document + ) { + name = await getNodeName( + document.getText(), + document.offsetAt(window.activeTextEditor?.selection.active), + currentModel.cachePath, + currentModel.framework, + currentModel.version + ).catch((Error) => { + console.log(Error); + return undefined; + }); + } + const ui5Url = `${new URL(apiRefUrl).href}${name ? "#/api/" + name : ""}`; + const openIn = workspace.getConfiguration().get(VIEW_API_REF); + if (openIn === "editor") { + const createWebView = () => { + currentPanel = window.createWebviewPanel( + "ui5APIRef", + "UI5 API Reference", + columnToShowIn, + { + enableScripts: true, + } + ); + currentPanel.iconPath = Uri.file( + resolve(__dirname, "..", "..", "resources", "logo_ui5.png") + ); + currentPanel.webview.html = getWebviewContent(ui5Url); + + // Reset when the current panel is closed + currentPanel.onDidDispose( + () => { + currentPanel = undefined; + }, + null, + extContext?.subscriptions + ); + }; + if (currentPanel) { + // If the panel is disposed, then create a new one. + try { + currentPanel.webview.postMessage({ url: ui5Url }); + + currentPanel.reveal(columnToShowIn); + } catch (error) { + currentPanel = undefined; + // Otherwise, create a new panel. + createWebView(); + } + } else { + createWebView(); + } + } else { + env.openExternal(Uri.parse(ui5Url)); + } +} + +function getWebviewContent(ui5Url: string) { + //Check if we are in a tag + + return ` + + + + + + + + + + `; +} + export function deactivate(): Thenable | undefined { if (!client) { return undefined; diff --git a/yarn.lock b/yarn.lock index 8f3b42bd1..9c8b5952a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8841,11 +8841,6 @@ tr46@^1.0.1: dependencies: punycode "^2.1.0" -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" - integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== - "traverse@>=0.3.0 <0.4": version "0.3.9" resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9"