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: SAP icons in editor or hover #512

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
4 changes: 4 additions & 0 deletions packages/semantic-model-types/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
94 changes: 93 additions & 1 deletion packages/vscode-ui5-language-assistant/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import {
commands,
env,
Uri,
OverviewRulerLane,
DecorationOptions,
Range,
DecorationRangeBehavior,
} from "vscode";
import {
LanguageClient,
Expand Down Expand Up @@ -46,7 +50,7 @@ export async function activate(context: ExtensionContext): Promise<void> {
window.onDidChangeActiveTextEditor(() => {
updateCurrentModel(undefined);
});

textDecorator(context);
client.start();
}

Expand Down Expand Up @@ -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<void> | undefined {
if (!client) {
return undefined;
Expand Down
41 changes: 41 additions & 0 deletions packages/xml-views-completion/src/providers/attributeValue/icon.ts
Original file line number Diff line number Diff line change
@@ -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,
// }));
}
Original file line number Diff line number Diff line change
@@ -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 = `
// <mvc:View
// xmlns:mvc="sap.ui.core.mvc"
// xmlns="sap.m">
// <Button icon = "⇶">
// </Button>
// </mvc:View>`;

// 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 = `
// <mvc:View
// xmlns:mvc="sap.ui.core.mvc"
// xmlns="sap.m">
// <Button icon = "⇶">
// </Button>
// </mvc:View>`;

// 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 = `
// <mvc:View
// xmlns:mvc="sap.ui.core.mvc"
// xmlns="sap.m">
// <Button icon = "⇶">
// </Button>
// </mvc:View>`;

// 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 = `
// <mvc:View
// xmlns:mvc="sap.ui.core.mvc"
// xmlns="sap.m">
// <List icon = "⇶">
// </List>
// </mvc:View>`;

// 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 = `
// <mvc:View
// xmlns:mvc="sap.ui.core.mvc"
// xmlns="sap.m">
// <Button ⇶>
// </Button>
// </mvc:View>`;

// 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 = `
// <mvc:View
// xmlns:mvc="sap.ui.core.mvc"
// xmlns="sap.m">
// <App homeIcon = "⇶">
// </App>
// </mvc:View>`;

// 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 = `
// <mvc:View
// xmlns:mvc="sap.ui.core.mvc"
// xmlns="sap.m">
// <Bamba icon = "⇶">
// </Bamba>
// </mvc:View>`;

// 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 = `
// <mvc:View
// xmlns:mvc="sap.ui.core.mvc"
// xmlns="sap.m">
// <Button UNKNOWN = "⇶">
// </Button>
// </mvc:View>`;

// 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);
// });
// }