Skip to content

Commit f4effa8

Browse files
committed
feat: icons in editor POC - failing tests
1 parent 352164f commit f4effa8

File tree

4 files changed

+353
-1
lines changed

4 files changed

+353
-1
lines changed

packages/semantic-model-types/api.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ export interface UI5EnumValue extends BaseUI5Node {
5858
kind: "UI5EnumValue";
5959
}
6060

61+
export interface UI5IconValue extends BaseUI5Node {
62+
kind: "UI5IconValue";
63+
}
64+
6165
export interface UI5Namespace extends BaseUI5Node {
6266
kind: "UI5Namespace";
6367
// Likely Not Relevant for XML.Views

packages/vscode-ui5-language-assistant/src/extension.ts

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ import {
1010
commands,
1111
env,
1212
Uri,
13+
OverviewRulerLane,
14+
DecorationOptions,
15+
Range,
16+
DecorationRangeBehavior,
1317
} from "vscode";
1418
import {
1519
LanguageClient,
@@ -46,7 +50,7 @@ export async function activate(context: ExtensionContext): Promise<void> {
4650
window.onDidChangeActiveTextEditor(() => {
4751
updateCurrentModel(undefined);
4852
});
49-
53+
textDecorator(context);
5054
client.start();
5155
}
5256

@@ -125,6 +129,94 @@ function updateCurrentModel(model: UI5Model | undefined) {
125129
}
126130
}
127131

132+
function textDecorator(context: ExtensionContext): void {
133+
let timeout: NodeJS.Timer | undefined = undefined;
134+
135+
// create a decorator type that we use to decorate small numbers
136+
const InlineIconDecoration = window.createTextEditorDecorationType({
137+
textDecoration: "none; opacity: 0.6 !important;",
138+
rangeBehavior: DecorationRangeBehavior.ClosedClosed,
139+
});
140+
141+
const HideTextDecoration = window.createTextEditorDecorationType({
142+
textDecoration: "none; display: none;", // a hack to inject custom style
143+
});
144+
145+
let activeEditor = window.activeTextEditor;
146+
147+
function updateDecorations() {
148+
if (!activeEditor) {
149+
return;
150+
}
151+
const regEx = /sap-icon:\/\/(\w+)/g;
152+
const text = activeEditor.document.getText();
153+
const decoratirOptions: DecorationOptions[] = [];
154+
let match;
155+
while ((match = regEx.exec(text))) {
156+
const startPos = activeEditor.document.positionAt(match.index);
157+
const endPos = activeEditor.document.positionAt(
158+
match.index + match[0].length
159+
);
160+
const item: DecorationOptions = {
161+
range: new Range(startPos, endPos),
162+
renderOptions: {
163+
before: {
164+
fontStyle: "SAP-icons",
165+
contentText: "",
166+
},
167+
},
168+
hoverMessage: "",
169+
};
170+
171+
decoratirOptions.push(item);
172+
}
173+
activeEditor.setDecorations(InlineIconDecoration, decoratirOptions);
174+
activeEditor.setDecorations(
175+
HideTextDecoration,
176+
decoratirOptions
177+
.map(({ range }) => range)
178+
.filter((i) => i.start.line !== activeEditor!.selection.start.line)
179+
);
180+
}
181+
182+
function triggerUpdateDecorations(throttle = false) {
183+
if (timeout) {
184+
clearTimeout(timeout);
185+
timeout = undefined;
186+
}
187+
if (throttle) {
188+
timeout = setTimeout(updateDecorations, 500);
189+
} else {
190+
updateDecorations();
191+
}
192+
}
193+
194+
if (activeEditor) {
195+
triggerUpdateDecorations();
196+
}
197+
198+
window.onDidChangeActiveTextEditor(
199+
(editor) => {
200+
activeEditor = editor;
201+
if (editor) {
202+
triggerUpdateDecorations();
203+
}
204+
},
205+
null,
206+
context.subscriptions
207+
);
208+
209+
workspace.onDidChangeTextDocument(
210+
(event) => {
211+
if (activeEditor && event.document === activeEditor.document) {
212+
triggerUpdateDecorations(true);
213+
}
214+
},
215+
null,
216+
context.subscriptions
217+
);
218+
}
219+
128220
export function deactivate(): Thenable<void> | undefined {
129221
if (!client) {
130222
return undefined;
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { map } from "lodash";
2+
import { XMLAttribute } from "@xml-tools/ast";
3+
import { getUI5PropertyByXMLAttributeKey } from "@ui5-language-assistant/logic-utils";
4+
import { UI5EnumsInXMLAttributeValueCompletion } from "../../../api";
5+
import { filterMembersForSuggestion } from "../utils/filter-members";
6+
import { UI5AttributeValueCompletionOptions } from "./index";
7+
import {
8+
UI5Field,
9+
UI5IconValue,
10+
} from "@ui5-language-assistant/semantic-model-types";
11+
12+
/**
13+
* Suggests Enum value inside Attribute
14+
* For example: 'ListSeparators' in 'showSeparators' attribute in `sap.m.ListBase` element
15+
*/
16+
export function iconSuggestions(
17+
opts: UI5AttributeValueCompletionOptions
18+
): void | UI5EnumsInXMLAttributeValueCompletion[] {
19+
const ui5Property = getUI5PropertyByXMLAttributeKey(
20+
opts.attribute,
21+
opts.context
22+
);
23+
const propType = ui5Property?.type;
24+
if (propType?.kind !== "UI5Namespace") {
25+
return [];
26+
}
27+
28+
const fields = propType.fields;
29+
const prefix = opts.prefix ?? "";
30+
const prefixMatchingIconValues: UI5Field[] = filterMembersForSuggestion(
31+
fields,
32+
prefix,
33+
[]
34+
);
35+
36+
// return map(prefixMatchingIconValues, (_) => ({
37+
// type: "UI5EnumsInXMLAttributeValue",
38+
// ui5Node: _,
39+
// astNode: opts.attribute as XMLAttribute,
40+
// }));
41+
}
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
// import { expect } from "chai";
2+
// import { forEach, map } from "lodash";
3+
// import { UI5SemanticModel } from "@ui5-language-assistant/semantic-model-types";
4+
// import { generateModel } from "@ui5-language-assistant/test-utils";
5+
// import { generate } from "@ui5-language-assistant/semantic-model";
6+
// import { XMLAttribute, XMLElement } from "@xml-tools/ast";
7+
// import { iconSuggestions } from "../../../src/providers/attributeValue/icon";
8+
// import { UI5XMLViewCompletion } from "../../../api";
9+
// import { testSuggestionsScenario } from "../../utils";
10+
11+
// describe("The ui5-language-assistant xml-views-completion", () => {
12+
// let ui5SemanticModel: UI5SemanticModel;
13+
// before(async function () {
14+
// ui5SemanticModel = await generateModel({
15+
// framework: "SAPUI5",
16+
// version: "1.71.49",
17+
// modelGenerator: generate,
18+
// });
19+
// });
20+
21+
// context("icon values", () => {
22+
// context("applicable scenarios", () => {
23+
// it("will suggest icon values with no prefix provided", () => {
24+
// const xmlSnippet = `
25+
// <mvc:View
26+
// xmlns:mvc="sap.ui.core.mvc"
27+
// xmlns="sap.m">
28+
// <Button icon = "⇶">
29+
// </Button>
30+
// </mvc:View>`;
31+
32+
// testSuggestionsScenario({
33+
// model: ui5SemanticModel,
34+
// xmlText: xmlSnippet,
35+
// providers: {
36+
// attributeValue: [iconSuggestions],
37+
// },
38+
// assertion: (suggestions) => {
39+
// const suggestedValues = map(suggestions, (_) => _.ui5Node.name);
40+
// expect(suggestedValues).to.deep.equalInAnyOrder([
41+
// "All",
42+
// "Inner",
43+
// "None",
44+
// ]);
45+
// expectIconValuesSuggestions(suggestions, "List");
46+
// },
47+
// });
48+
// });
49+
50+
// it("will suggest icon values filtered by prefix", () => {
51+
// const xmlSnippet = `
52+
// <mvc:View
53+
// xmlns:mvc="sap.ui.core.mvc"
54+
// xmlns="sap.m">
55+
// <Button icon = "⇶">
56+
// </Button>
57+
// </mvc:View>`;
58+
59+
// testSuggestionsScenario({
60+
// model: ui5SemanticModel,
61+
// xmlText: xmlSnippet,
62+
// providers: {
63+
// attributeValue: [iconSuggestions],
64+
// },
65+
// assertion: (suggestions) => {
66+
// const suggestedValues = map(suggestions, (_) => _.ui5Node.name);
67+
// expect(suggestedValues).to.deep.equalInAnyOrder(["Inner", "None"]);
68+
// expectIconValuesSuggestions(suggestions, "List");
69+
// },
70+
// });
71+
// });
72+
73+
// it("Will not suggest any icon values if none match the prefix", () => {
74+
// const xmlSnippet = `
75+
// <mvc:View
76+
// xmlns:mvc="sap.ui.core.mvc"
77+
// xmlns="sap.m">
78+
// <Button icon = "⇶">
79+
// </Button>
80+
// </mvc:View>`;
81+
82+
// testSuggestionsScenario({
83+
// model: ui5SemanticModel,
84+
// xmlText: xmlSnippet,
85+
// providers: {
86+
// attributeValue: [iconSuggestions],
87+
// },
88+
// assertion: (suggestions) => {
89+
// expect(suggestions).to.be.empty;
90+
// },
91+
// });
92+
// });
93+
// });
94+
95+
// context("none applicable scenarios", () => {
96+
// it("will not provide any suggestions when the property is not of icon type", () => {
97+
// const xmlSnippet = `
98+
// <mvc:View
99+
// xmlns:mvc="sap.ui.core.mvc"
100+
// xmlns="sap.m">
101+
// <List icon = "⇶">
102+
// </List>
103+
// </mvc:View>`;
104+
105+
// testSuggestionsScenario({
106+
// model: ui5SemanticModel,
107+
// xmlText: xmlSnippet,
108+
// providers: {
109+
// attributeValue: [iconSuggestions],
110+
// },
111+
// assertion: (suggestions) => {
112+
// expect(suggestions).to.be.empty;
113+
// },
114+
// });
115+
// });
116+
117+
// it("will not provide any suggestions when it is not an attribute value completion", () => {
118+
// const xmlSnippet = `
119+
// <mvc:View
120+
// xmlns:mvc="sap.ui.core.mvc"
121+
// xmlns="sap.m">
122+
// <Button ⇶>
123+
// </Button>
124+
// </mvc:View>`;
125+
126+
// testSuggestionsScenario({
127+
// model: ui5SemanticModel,
128+
// xmlText: xmlSnippet,
129+
// providers: {
130+
// attributeValue: [iconSuggestions],
131+
// },
132+
// assertion: (suggestions) => {
133+
// expect(suggestions).to.be.empty;
134+
// },
135+
// });
136+
// });
137+
138+
// it("will not provide any suggestions when the property type is undefined", () => {
139+
// const xmlSnippet = `
140+
// <mvc:View
141+
// xmlns:mvc="sap.ui.core.mvc"
142+
// xmlns="sap.m">
143+
// <App homeIcon = "⇶">
144+
// </App>
145+
// </mvc:View>`;
146+
147+
// testSuggestionsScenario({
148+
// model: ui5SemanticModel,
149+
// xmlText: xmlSnippet,
150+
// providers: {
151+
// attributeValue: [iconSuggestions],
152+
// },
153+
// assertion: (suggestions) => {
154+
// expect(suggestions).to.be.empty;
155+
// },
156+
// });
157+
// });
158+
159+
// it("will not provide any suggestions when not inside a UI5 Class", () => {
160+
// const xmlSnippet = `
161+
// <mvc:View
162+
// xmlns:mvc="sap.ui.core.mvc"
163+
// xmlns="sap.m">
164+
// <Bamba icon = "⇶">
165+
// </Bamba>
166+
// </mvc:View>`;
167+
168+
// testSuggestionsScenario({
169+
// model: ui5SemanticModel,
170+
// xmlText: xmlSnippet,
171+
// providers: {
172+
// attributeValue: [iconSuggestions],
173+
// },
174+
// assertion: (suggestions) => {
175+
// expect(ui5SemanticModel.classes["sap.ui.core.mvc.Bamba"]).to.be
176+
// .undefined;
177+
// expect(suggestions).to.be.empty;
178+
// },
179+
// });
180+
// });
181+
182+
// it("Will not suggest any enum values if there is no matching UI5 property", () => {
183+
// const xmlSnippet = `
184+
// <mvc:View
185+
// xmlns:mvc="sap.ui.core.mvc"
186+
// xmlns="sap.m">
187+
// <Button UNKNOWN = "⇶">
188+
// </Button>
189+
// </mvc:View>`;
190+
191+
// testSuggestionsScenario({
192+
// model: ui5SemanticModel,
193+
// xmlText: xmlSnippet,
194+
// providers: {
195+
// attributeValue: [iconSuggestions],
196+
// },
197+
// assertion: (suggestions) => {
198+
// expect(suggestions).to.be.empty;
199+
// },
200+
// });
201+
// });
202+
// });
203+
// });
204+
// });
205+
206+
// function expectIconValuesSuggestions(
207+
// suggestions: UI5XMLViewCompletion[],
208+
// expectedParentTag: string
209+
// ): void {
210+
// forEach(suggestions, (_) => {
211+
// expect(_.type).to.equal(`UI5IconInXMLAttributeValue`);
212+
// expect((_.astNode as XMLAttribute).key).to.equal("showSeparators");
213+
// expect((_.astNode.parent as XMLElement).name).to.equal(expectedParentTag);
214+
// });
215+
// }

0 commit comments

Comments
 (0)