Skip to content

Commit d6ceeaa

Browse files
authored
feat: Generate unique ID across view files in an application (#719)
* feat: support unique ID generation across a project * fix: validate all opened documents * fix: validate all opened documents on file delete or rename * fix: add and adapt tests * fix: add change set * fix: failing test and clean up * fix: remove dependency * fix: remove nyc. jest has build in coverage * fix: remove nyc cofig.js * refactor: cache control in context and avoid context manipulation * fix: change set * fix: escpe SonarCloud reporting * fix: snoare cloud issues * fix: review comments and small improvment * fix: performance optimization * chore: inclusive language
1 parent bcd5523 commit d6ceeaa

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+1960
-933
lines changed

.changeset/spicy-trainers-vanish.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@ui5-language-assistant/vscode-ui5-language-assistant-bas-ext": patch
3+
"vscode-ui5-language-assistant": patch
4+
"@ui5-language-assistant/context": patch
5+
"@ui5-language-assistant/language-server": patch
6+
"@ui5-language-assistant/logic-utils",: patch
7+
"@ui5-language-assistant/user-facing-text": patch
8+
"@ui5-language-assistant/xml-views-completion": patch
9+
"@ui5-language-assistant/xml-views-definition": patch
10+
"@ui5-language-assistant/xml-views-quick-fix": patch
11+
"@ui5-language-assistant/xml-views-tooltip": patch
12+
"@ui5-language-assistant/xml-views-validation": patch
13+
---
14+
15+
feat: support unique id generation across view files in an application

nyc.config.js

Lines changed: 0 additions & 10 deletions
This file was deleted.

package.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"build:quick": "lerna run compile && lerna run bundle && lerna run package",
1818
"release:version": "lerna version --force-publish",
1919
"release:publish": "lerna publish from-package --yes",
20-
"ci": "npm-run-all format:validate ci:subpackages coverage:merge legal:*",
20+
"ci": "npm-run-all format:validate ci:subpackages legal:*",
2121
"compile": "yarn run clean && tsc --build",
2222
"compile:watch": "yarn run clean && tsc --build --watch",
2323
"format:fix": "prettier --write \"**/*.@(js|ts|json|md)\" --ignore-path=.gitignore",
@@ -26,7 +26,6 @@
2626
"ci:subpackages": "lerna run ci",
2727
"test": "lerna run test",
2828
"coverage": "lerna run coverage",
29-
"coverage:merge": "node ./scripts/merge-coverage",
3029
"clean": "lerna run clean",
3130
"update-snapshots": "lerna run update-snapshots",
3231
"legal:delete": "lerna exec \"shx rm -rf .reuse LICENSES\" || true",
@@ -80,7 +79,6 @@
8079
"make-dir": "3.1.0",
8180
"mock-fs": "^5.2.0",
8281
"npm-run-all": "4.1.5",
83-
"nyc": "15.1.0",
8482
"prettier": "2.8.7",
8583
"rimraf": "3.0.2",
8684
"shx": "0.3.3",

packages/context/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@
2929
"lodash": "4.17.21",
3030
"semver": "7.3.7",
3131
"vscode-languageserver": "8.0.2",
32-
"vscode-uri": "2.1.2"
32+
"vscode-uri": "2.1.2",
33+
"@xml-tools/ast": "5.0.0",
34+
"@xml-tools/parser": "1.0.7"
3335
},
3436
"devDependencies": {
3537
"@sap-ux/vocabularies-types": "0.10.14",

packages/context/src/api.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import { getServices } from "./services";
88
import { Context } from "./types";
99
import { getSemanticModel } from "./ui5-model";
1010
import { getYamlDetails } from "./ui5-yaml";
11+
import { getViewFiles } from "./utils/view-files";
12+
import { getControlIds } from "./utils/control-ids";
13+
import { getLogger } from "./utils";
1114

1215
export {
1316
initializeManifestData,
@@ -27,17 +30,20 @@ export {
2730
reactOnUI5YamlChange,
2831
reactOnManifestChange,
2932
reactOnXmlFileChange,
33+
reactOnViewFileChange,
3034
reactOnPackageJson,
3135
} from "./watcher";
3236

3337
/**
3438
* Get context for a file
3539
* @param documentPath path to a file e.g. absolute/path/webapp/ext/main/Main.view.xml
3640
* @param modelCachePath path to a cached UI5 model
41+
* @param content document content. If provided, it will re-parse and re-assign it to current document of xml views
3742
*/
3843
export async function getContext(
3944
documentPath: string,
40-
modelCachePath?: string
45+
modelCachePath?: string,
46+
content?: string
4147
): Promise<Context | Error> {
4248
try {
4349
const manifestDetails = await getManifestDetails(documentPath);
@@ -55,8 +61,31 @@ export async function getContext(
5561
);
5662
const services = await getServices(documentPath);
5763
const customViewId = await getCustomViewId(documentPath);
58-
return { manifestDetails, yamlDetails, ui5Model, services, customViewId };
64+
const manifestPath = manifestDetails.manifestPath;
65+
const viewFiles = await getViewFiles({
66+
manifestPath,
67+
documentPath,
68+
content,
69+
});
70+
const controlIds = getControlIds({
71+
manifestPath,
72+
documentPath,
73+
content,
74+
});
75+
return {
76+
manifestDetails,
77+
yamlDetails,
78+
ui5Model,
79+
services,
80+
customViewId,
81+
viewFiles,
82+
controlIds,
83+
documentPath,
84+
};
5985
} catch (error) {
86+
getLogger().debug("getContext failed:", {
87+
error,
88+
});
6089
return error as Error;
6190
}
6291
}

packages/context/src/cache.ts

Lines changed: 105 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import type { Manifest } from "@sap-ux/project-access";
22
import type { UI5SemanticModel } from "@ui5-language-assistant/semantic-model-types";
3-
4-
import type { App, Project, YamlDetails } from "./types";
3+
import { accept, type XMLDocument } from "@xml-tools/ast";
4+
import type { App, ControlIdLocation, Project, YamlDetails } from "./types";
5+
import { createDocumentAst, IdsCollectorVisitor } from "./utils";
6+
import { FileChangeType } from "vscode-languageserver/node";
57

68
type AbsoluteAppRoot = string;
79
type AbsoluteProjectRoot = string;
@@ -13,13 +15,20 @@ class Cache {
1315
private CAPServices: Map<AbsoluteProjectRoot, Map<string, string>>;
1416
private ui5YamlDetails: Map<string, YamlDetails>;
1517
private ui5Model: Map<string, UI5SemanticModel>;
18+
private viewFiles: Map<string, Record<string, XMLDocument>>;
19+
private controlIds: Map<
20+
string,
21+
Record<string, Map<string, ControlIdLocation[]>>
22+
>;
1623
constructor() {
1724
this.project = new Map();
1825
this.manifest = new Map();
1926
this.app = new Map();
2027
this.CAPServices = new Map();
2128
this.ui5YamlDetails = new Map();
2229
this.ui5Model = new Map();
30+
this.viewFiles = new Map();
31+
this.controlIds = new Map();
2332
}
2433
reset() {
2534
this.project = new Map();
@@ -28,6 +37,8 @@ class Cache {
2837
this.CAPServices = new Map();
2938
this.ui5YamlDetails = new Map();
3039
this.ui5Model = new Map();
40+
this.viewFiles = new Map();
41+
this.controlIds = new Map();
3142
}
3243
/**
3344
* Get entries of cached project
@@ -124,6 +135,98 @@ class Cache {
124135
deleteUI5Model(key: string): boolean {
125136
return this.ui5Model.delete(key);
126137
}
138+
/**
139+
* Get entries of view files
140+
*/
141+
getViewFiles(manifestPath: string): Record<string, XMLDocument> {
142+
return this.viewFiles.get(manifestPath) ?? {};
143+
}
144+
145+
setViewFiles(
146+
manifestPath: string,
147+
viewFiles: Record<string, XMLDocument>
148+
): void {
149+
this.viewFiles.set(manifestPath, viewFiles);
150+
}
151+
152+
/**
153+
* Set view file. Use this API to manipulate cache for single view file. This is to avoid cache manipulation outside of cache file.
154+
*
155+
* @param param - The parameter object
156+
* @param param.manifestPath - The path to the manifest.json
157+
* @param param.documentPath - The path to the document
158+
* @param param.operation - The operation to be performed (create or delete)
159+
* @param param.content - The content of the document (optional, only required for 'create' operation)
160+
* @returns - A Promise that resolves to void
161+
*/
162+
async setViewFile(param: {
163+
manifestPath: string;
164+
documentPath: string;
165+
operation: Exclude<FileChangeType, 2>;
166+
content?: string;
167+
}): Promise<void> {
168+
const { manifestPath, documentPath, operation, content } = param;
169+
if (operation === FileChangeType.Created) {
170+
const viewFiles = this.getViewFiles(manifestPath);
171+
viewFiles[documentPath] = await createDocumentAst(documentPath, content);
172+
// assign new view files to cache
173+
this.setViewFiles(manifestPath, viewFiles);
174+
return;
175+
}
176+
177+
const viewFiles = this.getViewFiles(manifestPath);
178+
delete viewFiles[documentPath];
179+
// assign new view files to cache
180+
this.setViewFiles(manifestPath, viewFiles);
181+
}
182+
/**
183+
* Get entries of control ids
184+
*/
185+
getControlIds(
186+
manifestPath: string
187+
): Record<string, Map<string, ControlIdLocation[]>> {
188+
return this.controlIds.get(manifestPath) ?? {};
189+
}
190+
setControlIds(
191+
manifestPath: string,
192+
controlIds: Record<string, Map<string, ControlIdLocation[]>>
193+
) {
194+
this.controlIds.set(manifestPath, controlIds);
195+
}
196+
197+
/**
198+
* Set control's id for xml view. Use this API to manipulate cache for controls ids of a single view file. This is to avoid cache manipulation out side of cache file.
199+
*
200+
* @param manifestPath - The path to the manifest.json
201+
* @param documentPath - The path to the document
202+
* @param param.operation - The operation to be performed (create or delete)
203+
*/
204+
setControlIdsForViewFile(param: {
205+
manifestPath: string;
206+
documentPath: string;
207+
operation: Exclude<FileChangeType, 2>;
208+
}): void {
209+
const { manifestPath, documentPath, operation } = param;
210+
211+
if (operation === FileChangeType.Created) {
212+
const viewFiles = this.getViewFiles(manifestPath);
213+
// for current document, re-collect and re-assign it to avoid cache issue
214+
if (viewFiles[documentPath]) {
215+
const idCollector = new IdsCollectorVisitor(documentPath);
216+
accept(viewFiles[documentPath], idCollector);
217+
const idControls = this.getControlIds(manifestPath);
218+
idControls[documentPath] = idCollector.getControlIds();
219+
// assign new control ids to cache
220+
this.setControlIds(manifestPath, idControls);
221+
}
222+
return;
223+
}
224+
225+
const idControls = this.getControlIds(manifestPath);
226+
delete idControls[documentPath];
227+
// assign new control ids to cache
228+
this.setControlIds(manifestPath, idControls);
229+
}
127230
}
128231

129232
/**

packages/context/src/types.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@ import type {
44
} from "@ui5-language-assistant/semantic-model-types";
55
import { ConvertedMetadata } from "@sap-ux/vocabularies-types";
66
import type { Manifest } from "@sap-ux/project-access";
7-
import { FetchResponse } from "@ui5-language-assistant/logic-utils";
7+
import {
8+
FetchResponse,
9+
OffsetRange,
10+
} from "@ui5-language-assistant/logic-utils";
11+
import type { XMLDocument } from "@xml-tools/ast";
12+
import { Location } from "vscode-languageserver";
813

914
export const UI5_VERSION_S4_PLACEHOLDER = "${sap.ui5.dist.version}";
1015

@@ -18,12 +23,19 @@ export enum DirName {
1823
Ext = "ext",
1924
}
2025

26+
export interface ControlIdLocation extends Location {
27+
offsetRange: OffsetRange;
28+
}
29+
2130
export interface Context {
2231
ui5Model: UI5SemanticModel;
2332
manifestDetails: ManifestDetails;
2433
yamlDetails: YamlDetails;
2534
services: Record<string, ServiceDetails>;
2635
customViewId: string;
36+
viewFiles: Record<string, XMLDocument>;
37+
controlIds: Map<string, ControlIdLocation[]>;
38+
documentPath: string;
2739
}
2840

2941
/**
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { FileChangeType } from "vscode-languageserver/node";
2+
import { cache } from "../api";
3+
import { ControlIdLocation } from "../types";
4+
import { IdsCollectorVisitor } from "./ids-collector";
5+
import { accept } from "@xml-tools/ast";
6+
7+
/**
8+
* Process control ids
9+
*
10+
* @param param parameter object
11+
* @param param.manifestPath path to manifest.json file
12+
* @param param.documentPath path to xml view file
13+
*/
14+
function processControlIds(param: {
15+
manifestPath: string;
16+
documentPath: string;
17+
content?: string;
18+
}): void {
19+
const { documentPath, manifestPath, content } = param;
20+
// check cache
21+
if (Object.keys(cache.getControlIds(manifestPath)).length > 0) {
22+
if (content) {
23+
// for current document, re-collect and re-assign it to avoid cache issue
24+
cache.setControlIdsForViewFile({
25+
manifestPath,
26+
documentPath,
27+
operation: FileChangeType.Created,
28+
});
29+
}
30+
return;
31+
}
32+
33+
// build fresh
34+
const ctrIds: Record<string, Map<string, ControlIdLocation[]>> = {};
35+
const viewFiles = cache.getViewFiles(manifestPath);
36+
const files = Object.keys(viewFiles);
37+
for (const docPath of files) {
38+
const idCollector = new IdsCollectorVisitor(docPath);
39+
accept(viewFiles[docPath], idCollector);
40+
ctrIds[docPath] = idCollector.getControlIds();
41+
}
42+
cache.setControlIds(manifestPath, ctrIds);
43+
}
44+
45+
/**
46+
* Get control ids of all xml files.
47+
*
48+
* @param param parameter object
49+
* @param param.manifestPath path to manifest.json file
50+
* @param param.documentPath path to xml view file
51+
* @returns merged control ids of all xml files
52+
*/
53+
export function getControlIds(param: {
54+
manifestPath: string;
55+
documentPath: string;
56+
content?: string;
57+
}): Map<string, ControlIdLocation[]> {
58+
const { manifestPath } = param;
59+
60+
processControlIds(param);
61+
62+
const allDocumentsIds = cache.getControlIds(manifestPath);
63+
const keys = Object.keys(allDocumentsIds);
64+
65+
const mergedIds: Map<string, ControlIdLocation[]> = new Map();
66+
for (const doc of keys) {
67+
const ids = allDocumentsIds[doc];
68+
for (const [id, location] of ids) {
69+
const existing = mergedIds.get(id);
70+
if (existing) {
71+
mergedIds.set(id, [...existing, ...location]);
72+
} else {
73+
mergedIds.set(id, location);
74+
}
75+
}
76+
}
77+
return mergedIds;
78+
}

0 commit comments

Comments
 (0)