diff --git a/packages/semantic-model-types/api.d.ts b/packages/semantic-model-types/api.d.ts index c9288a447..194eda87d 100644 --- a/packages/semantic-model-types/api.d.ts +++ b/packages/semantic-model-types/api.d.ts @@ -58,6 +58,10 @@ export interface UI5EnumValue extends BaseUI5Node { kind: "UI5EnumValue"; } +export interface UI5IconValue extends BaseUI5Node { + kind: "UI5IconValue"; +} + export interface UI5Namespace extends BaseUI5Node { kind: "UI5Namespace"; // Likely Not Relevant for XML.Views diff --git a/packages/vscode-ui5-language-assistant/src/extension.ts b/packages/vscode-ui5-language-assistant/src/extension.ts index ccfc0ff52..9d670f7c1 100644 --- a/packages/vscode-ui5-language-assistant/src/extension.ts +++ b/packages/vscode-ui5-language-assistant/src/extension.ts @@ -10,6 +10,10 @@ import { commands, env, Uri, + OverviewRulerLane, + DecorationOptions, + Range, + DecorationRangeBehavior, } from "vscode"; import { LanguageClient, @@ -46,7 +50,7 @@ export async function activate(context: ExtensionContext): Promise { window.onDidChangeActiveTextEditor(() => { updateCurrentModel(undefined); }); - + textDecorator(context); client.start(); } @@ -125,6 +129,94 @@ function updateCurrentModel(model: UI5Model | undefined) { } } +function textDecorator(context: ExtensionContext): void { + let timeout: NodeJS.Timer | undefined = undefined; + + // create a decorator type that we use to decorate small numbers + const InlineIconDecoration = window.createTextEditorDecorationType({ + textDecoration: "none; opacity: 0.6 !important;", + rangeBehavior: DecorationRangeBehavior.ClosedClosed, + }); + + const HideTextDecoration = window.createTextEditorDecorationType({ + textDecoration: "none; display: none;", // a hack to inject custom style + }); + + let activeEditor = window.activeTextEditor; + + function updateDecorations() { + if (!activeEditor) { + return; + } + const regEx = /sap-icon:\/\/(\w+)/g; + const text = activeEditor.document.getText(); + const decoratirOptions: DecorationOptions[] = []; + let match; + while ((match = regEx.exec(text))) { + const startPos = activeEditor.document.positionAt(match.index); + const endPos = activeEditor.document.positionAt( + match.index + match[0].length + ); + const item: DecorationOptions = { + range: new Range(startPos, endPos), + renderOptions: { + before: { + fontStyle: "SAP-icons", + contentText: "", + }, + }, + hoverMessage: "", + }; + + decoratirOptions.push(item); + } + activeEditor.setDecorations(InlineIconDecoration, decoratirOptions); + activeEditor.setDecorations( + HideTextDecoration, + decoratirOptions + .map(({ range }) => range) + .filter((i) => i.start.line !== activeEditor!.selection.start.line) + ); + } + + function triggerUpdateDecorations(throttle = false) { + if (timeout) { + clearTimeout(timeout); + timeout = undefined; + } + if (throttle) { + timeout = setTimeout(updateDecorations, 500); + } else { + updateDecorations(); + } + } + + if (activeEditor) { + triggerUpdateDecorations(); + } + + window.onDidChangeActiveTextEditor( + (editor) => { + activeEditor = editor; + if (editor) { + triggerUpdateDecorations(); + } + }, + null, + context.subscriptions + ); + + workspace.onDidChangeTextDocument( + (event) => { + if (activeEditor && event.document === activeEditor.document) { + triggerUpdateDecorations(true); + } + }, + null, + context.subscriptions + ); +} + export function deactivate(): Thenable | undefined { if (!client) { return undefined; diff --git a/packages/xml-views-completion/src/providers/attributeValue/icon.ts b/packages/xml-views-completion/src/providers/attributeValue/icon.ts new file mode 100644 index 000000000..843c635d5 --- /dev/null +++ b/packages/xml-views-completion/src/providers/attributeValue/icon.ts @@ -0,0 +1,41 @@ +import { map } from "lodash"; +import { XMLAttribute } from "@xml-tools/ast"; +import { getUI5PropertyByXMLAttributeKey } from "@ui5-language-assistant/logic-utils"; +import { UI5EnumsInXMLAttributeValueCompletion } from "../../../api"; +import { filterMembersForSuggestion } from "../utils/filter-members"; +import { UI5AttributeValueCompletionOptions } from "./index"; +import { + UI5Field, + UI5IconValue, +} from "@ui5-language-assistant/semantic-model-types"; + +/** + * Suggests Enum value inside Attribute + * For example: 'ListSeparators' in 'showSeparators' attribute in `sap.m.ListBase` element + */ +export function iconSuggestions( + opts: UI5AttributeValueCompletionOptions +): void | UI5EnumsInXMLAttributeValueCompletion[] { + const ui5Property = getUI5PropertyByXMLAttributeKey( + opts.attribute, + opts.context + ); + const propType = ui5Property?.type; + if (propType?.kind !== "UI5Namespace") { + return []; + } + + const fields = propType.fields; + const prefix = opts.prefix ?? ""; + const prefixMatchingIconValues: UI5Field[] = filterMembersForSuggestion( + fields, + prefix, + [] + ); + + // return map(prefixMatchingIconValues, (_) => ({ + // type: "UI5EnumsInXMLAttributeValue", + // ui5Node: _, + // astNode: opts.attribute as XMLAttribute, + // })); +} diff --git a/packages/xml-views-completion/test/providers/attributeValue/icon-spec.ts b/packages/xml-views-completion/test/providers/attributeValue/icon-spec.ts new file mode 100644 index 000000000..763854dd1 --- /dev/null +++ b/packages/xml-views-completion/test/providers/attributeValue/icon-spec.ts @@ -0,0 +1,215 @@ +// import { expect } from "chai"; +// import { forEach, map } from "lodash"; +// import { UI5SemanticModel } from "@ui5-language-assistant/semantic-model-types"; +// import { generateModel } from "@ui5-language-assistant/test-utils"; +// import { generate } from "@ui5-language-assistant/semantic-model"; +// import { XMLAttribute, XMLElement } from "@xml-tools/ast"; +// import { iconSuggestions } from "../../../src/providers/attributeValue/icon"; +// import { UI5XMLViewCompletion } from "../../../api"; +// import { testSuggestionsScenario } from "../../utils"; + +// describe("The ui5-language-assistant xml-views-completion", () => { +// let ui5SemanticModel: UI5SemanticModel; +// before(async function () { +// ui5SemanticModel = await generateModel({ +// framework: "SAPUI5", +// version: "1.71.49", +// modelGenerator: generate, +// }); +// }); + +// context("icon values", () => { +// context("applicable scenarios", () => { +// it("will suggest icon values with no prefix provided", () => { +// const xmlSnippet = ` +// +// +// `; + +// testSuggestionsScenario({ +// model: ui5SemanticModel, +// xmlText: xmlSnippet, +// providers: { +// attributeValue: [iconSuggestions], +// }, +// assertion: (suggestions) => { +// const suggestedValues = map(suggestions, (_) => _.ui5Node.name); +// expect(suggestedValues).to.deep.equalInAnyOrder([ +// "All", +// "Inner", +// "None", +// ]); +// expectIconValuesSuggestions(suggestions, "List"); +// }, +// }); +// }); + +// it("will suggest icon values filtered by prefix", () => { +// const xmlSnippet = ` +// +// +// `; + +// testSuggestionsScenario({ +// model: ui5SemanticModel, +// xmlText: xmlSnippet, +// providers: { +// attributeValue: [iconSuggestions], +// }, +// assertion: (suggestions) => { +// const suggestedValues = map(suggestions, (_) => _.ui5Node.name); +// expect(suggestedValues).to.deep.equalInAnyOrder(["Inner", "None"]); +// expectIconValuesSuggestions(suggestions, "List"); +// }, +// }); +// }); + +// it("Will not suggest any icon values if none match the prefix", () => { +// const xmlSnippet = ` +// +// +// `; + +// testSuggestionsScenario({ +// model: ui5SemanticModel, +// xmlText: xmlSnippet, +// providers: { +// attributeValue: [iconSuggestions], +// }, +// assertion: (suggestions) => { +// expect(suggestions).to.be.empty; +// }, +// }); +// }); +// }); + +// context("none applicable scenarios", () => { +// it("will not provide any suggestions when the property is not of icon type", () => { +// const xmlSnippet = ` +// +// +// +// `; + +// testSuggestionsScenario({ +// model: ui5SemanticModel, +// xmlText: xmlSnippet, +// providers: { +// attributeValue: [iconSuggestions], +// }, +// assertion: (suggestions) => { +// expect(suggestions).to.be.empty; +// }, +// }); +// }); + +// it("will not provide any suggestions when it is not an attribute value completion", () => { +// const xmlSnippet = ` +// +// +// `; + +// testSuggestionsScenario({ +// model: ui5SemanticModel, +// xmlText: xmlSnippet, +// providers: { +// attributeValue: [iconSuggestions], +// }, +// assertion: (suggestions) => { +// expect(suggestions).to.be.empty; +// }, +// }); +// }); + +// it("will not provide any suggestions when the property type is undefined", () => { +// const xmlSnippet = ` +// +// +// +// `; + +// testSuggestionsScenario({ +// model: ui5SemanticModel, +// xmlText: xmlSnippet, +// providers: { +// attributeValue: [iconSuggestions], +// }, +// assertion: (suggestions) => { +// expect(suggestions).to.be.empty; +// }, +// }); +// }); + +// it("will not provide any suggestions when not inside a UI5 Class", () => { +// const xmlSnippet = ` +// +// +// +// `; + +// testSuggestionsScenario({ +// model: ui5SemanticModel, +// xmlText: xmlSnippet, +// providers: { +// attributeValue: [iconSuggestions], +// }, +// assertion: (suggestions) => { +// expect(ui5SemanticModel.classes["sap.ui.core.mvc.Bamba"]).to.be +// .undefined; +// expect(suggestions).to.be.empty; +// }, +// }); +// }); + +// it("Will not suggest any enum values if there is no matching UI5 property", () => { +// const xmlSnippet = ` +// +// +// `; + +// testSuggestionsScenario({ +// model: ui5SemanticModel, +// xmlText: xmlSnippet, +// providers: { +// attributeValue: [iconSuggestions], +// }, +// assertion: (suggestions) => { +// expect(suggestions).to.be.empty; +// }, +// }); +// }); +// }); +// }); +// }); + +// function expectIconValuesSuggestions( +// suggestions: UI5XMLViewCompletion[], +// expectedParentTag: string +// ): void { +// forEach(suggestions, (_) => { +// expect(_.type).to.equal(`UI5IconInXMLAttributeValue`); +// expect((_.astNode as XMLAttribute).key).to.equal("showSeparators"); +// expect((_.astNode.parent as XMLElement).name).to.equal(expectedParentTag); +// }); +// }