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: Generate unique ID across view files in an application #719

Merged
merged 21 commits into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
965c2f7
feat: support unique ID generation across a project
marufrasully Aug 2, 2024
6b0a6be
fix: validate all opened documents
marufrasully Aug 2, 2024
1c102b3
fix: validate all opened documents on file delete or rename
marufrasully Aug 2, 2024
bf260af
fix: add and adapt tests
marufrasully Aug 7, 2024
ad25972
Merge remote-tracking branch 'origin/master' into feat/generate-id
marufrasully Aug 7, 2024
151616f
fix: add change set
marufrasully Aug 7, 2024
7d30ae9
fix: failing test and clean up
marufrasully Aug 7, 2024
f1735ea
fix: remove dependency
marufrasully Aug 7, 2024
6cda198
fix: remove nyc. jest has build in coverage
marufrasully Aug 8, 2024
fec1fe8
fix: remove nyc cofig.js
marufrasully Aug 8, 2024
bbd1637
Merge remote-tracking branch 'origin' into feat/generate-id
marufrasully Aug 12, 2024
0e4bdd4
Merge remote-tracking branch 'origin/master' into feat/generate-id
marufrasully Aug 13, 2024
8b78a60
refactor: cache control in context and avoid context manipulation
marufrasully Aug 16, 2024
d5dc4a4
fix: change set
marufrasully Aug 16, 2024
8b51020
Merge remote-tracking branch 'origin/master' into feat/generate-id
marufrasully Aug 16, 2024
a26da65
fix: escpe SonarCloud reporting
marufrasully Aug 16, 2024
504019e
fix: snoare cloud issues
marufrasully Aug 19, 2024
eb374cf
fix: review comments and small improvment
marufrasully Aug 21, 2024
232dfcc
Merge remote-tracking branch 'origin/master' into feat/generate-id
marufrasully Aug 21, 2024
3f472d5
fix: performance optimization
marufrasully Aug 21, 2024
e21e313
chore: inclusive language
marufrasully Aug 21, 2024
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
15 changes: 15 additions & 0 deletions .changeset/spicy-trainers-vanish.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"@ui5-language-assistant/vscode-ui5-language-assistant-bas-ext": patch
"vscode-ui5-language-assistant": patch
"@ui5-language-assistant/context": patch
"@ui5-language-assistant/language-server": patch
"@ui5-language-assistant/logic-utils",: patch
"@ui5-language-assistant/user-facing-text": patch
"@ui5-language-assistant/xml-views-completion": patch
"@ui5-language-assistant/xml-views-definition": patch
"@ui5-language-assistant/xml-views-quick-fix": patch
"@ui5-language-assistant/xml-views-tooltip": patch
"@ui5-language-assistant/xml-views-validation": patch
---

feat: support unique id generation across view files in an application
10 changes: 0 additions & 10 deletions nyc.config.js

This file was deleted.

4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"build:quick": "lerna run compile && lerna run bundle && lerna run package",
"release:version": "lerna version --force-publish",
"release:publish": "lerna publish from-package --yes",
"ci": "npm-run-all format:validate ci:subpackages coverage:merge legal:*",
"ci": "npm-run-all format:validate ci:subpackages legal:*",
"compile": "yarn run clean && tsc --build",
"compile:watch": "yarn run clean && tsc --build --watch",
"format:fix": "prettier --write \"**/*.@(js|ts|json|md)\" --ignore-path=.gitignore",
Expand All @@ -26,7 +26,6 @@
"ci:subpackages": "lerna run ci",
"test": "lerna run test",
"coverage": "lerna run coverage",
"coverage:merge": "node ./scripts/merge-coverage",
"clean": "lerna run clean",
"update-snapshots": "lerna run update-snapshots",
"legal:delete": "lerna exec \"shx rm -rf .reuse LICENSES\" || true",
Expand Down Expand Up @@ -80,7 +79,6 @@
"make-dir": "3.1.0",
"mock-fs": "^5.2.0",
"npm-run-all": "4.1.5",
"nyc": "15.1.0",
"prettier": "2.8.7",
"rimraf": "3.0.2",
"shx": "0.3.3",
Expand Down
4 changes: 3 additions & 1 deletion packages/context/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@
"lodash": "4.17.21",
"semver": "7.3.7",
"vscode-languageserver": "8.0.2",
"vscode-uri": "2.1.2"
"vscode-uri": "2.1.2",
"@xml-tools/ast": "5.0.0",
"@xml-tools/parser": "1.0.7"
},
"devDependencies": {
"@sap-ux/vocabularies-types": "0.10.14",
Expand Down
32 changes: 30 additions & 2 deletions packages/context/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import { getServices } from "./services";
import { Context } from "./types";
import { getSemanticModel } from "./ui5-model";
import { getYamlDetails } from "./ui5-yaml";
import { getViewFiles } from "./utils/view-files";
import { getControlIds } from "./utils/control-ids";
import { getLogger } from "./utils";

export {
initializeManifestData,
Expand All @@ -27,17 +30,20 @@ export {
reactOnUI5YamlChange,
reactOnManifestChange,
reactOnXmlFileChange,
reactOnViewFileChange,
reactOnPackageJson,
} from "./watcher";

/**
* Get context for a file
* @param documentPath path to a file e.g. absolute/path/webapp/ext/main/Main.view.xml
* @param modelCachePath path to a cached UI5 model
* @param content document content. If provided, it will re-parse and re-assign it to current document of xml views
*/
export async function getContext(
documentPath: string,
modelCachePath?: string
modelCachePath?: string,
content?: string
): Promise<Context | Error> {
try {
const manifestDetails = await getManifestDetails(documentPath);
Expand All @@ -55,8 +61,30 @@ export async function getContext(
);
const services = await getServices(documentPath);
const customViewId = await getCustomViewId(documentPath);
return { manifestDetails, yamlDetails, ui5Model, services, customViewId };
const manifestPath = manifestDetails.manifestPath;
const viewFiles = await getViewFiles({
manifestPath,
documentPath,
content,
});
const controlIds = getControlIds({
manifestPath,
documentPath,
});
return {
manifestDetails,
yamlDetails,
ui5Model,
services,
customViewId,
viewFiles,
controlIds,
documentPath,
};
} catch (error) {
getLogger().debug("getContext failed:", {
error,
});
return error as Error;
}
}
Expand Down
101 changes: 99 additions & 2 deletions packages/context/src/cache.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { Manifest } from "@sap-ux/project-access";
import type { UI5SemanticModel } from "@ui5-language-assistant/semantic-model-types";

import type { App, Project, YamlDetails } from "./types";
import { accept, type XMLDocument } from "@xml-tools/ast";
import type { App, ControlIdLocation, Project, YamlDetails } from "./types";
import { createDocumentAst, IdsCollectorVisitor } from "./utils";

type AbsoluteAppRoot = string;
type AbsoluteProjectRoot = string;
Expand All @@ -13,13 +14,20 @@ class Cache {
private CAPServices: Map<AbsoluteProjectRoot, Map<string, string>>;
private ui5YamlDetails: Map<string, YamlDetails>;
private ui5Model: Map<string, UI5SemanticModel>;
private viewFiles: Record<string, Record<string, XMLDocument>>;
private controlIds: Record<
string,
Record<string, Map<string, ControlIdLocation[]>>
>;
constructor() {
this.project = new Map();
this.manifest = new Map();
this.app = new Map();
this.CAPServices = new Map();
this.ui5YamlDetails = new Map();
this.ui5Model = new Map();
this.viewFiles = {};
this.controlIds = {};
}
reset() {
this.project = new Map();
Expand All @@ -28,6 +36,8 @@ class Cache {
this.CAPServices = new Map();
this.ui5YamlDetails = new Map();
this.ui5Model = new Map();
this.viewFiles = {};
this.controlIds = {};
}
/**
* Get entries of cached project
Expand Down Expand Up @@ -124,6 +134,93 @@ class Cache {
deleteUI5Model(key: string): boolean {
return this.ui5Model.delete(key);
}
/**
* Get entries of view files
*/
getViewFiles(manifestPath: string): Record<string, XMLDocument> {
return this.viewFiles[manifestPath] ?? {};
}

setViewFiles(
manifestPath: string,
viewFiles: Record<string, XMLDocument>
): void {
this.viewFiles[manifestPath] = viewFiles;
}

/**
* Set view file. Use this API to manipulate cache for single view file. This is to avoid cache manipulation outside of cache file.
*
* @param param - The parameter object
* @param param.manifestPath - The path to the manifest.json
* @param param.documentPath - The path to the document
* @param param.operation - The operation to be performed (create or delete)
* @param param.content - The content of the document (optional, only required for 'create' operation)
* @returns - A Promise that resolves to void
*/
async setViewFile(param: {
manifestPath: string;
documentPath: string;
operation: "create" | "delete";
content?: string;
}): Promise<void> {
const { manifestPath, documentPath, operation, content } = param;
if (operation === "create") {
const viewFiles = this.getViewFiles(manifestPath);
viewFiles[documentPath] = await createDocumentAst(documentPath, content);
this.setViewFiles(manifestPath, viewFiles);
return;
}

const viewFiles = this.getViewFiles(manifestPath);
delete viewFiles[documentPath];
this.setViewFiles(manifestPath, viewFiles);
}
/**
* Get entries of control ids
*/
getControlIds(
manifestPath: string
): Record<string, Map<string, ControlIdLocation[]>> {
return this.controlIds[manifestPath] ?? {};
}
setControlIds(
manifestPath: string,
controlIds: Record<string, Map<string, ControlIdLocation[]>>
) {
this.controlIds[manifestPath] = controlIds;
}

/**
* 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.
*
* @param manifestPath - The path to the manifest.json
* @param documentPath - The path to the document
* @param param.operation - The operation to be performed (create or delete)
*/
setControlIdsForViewFile(param: {
manifestPath: string;
documentPath: string;
operation: "create" | "delete";
}): void {
const { manifestPath, documentPath, operation } = param;

if (operation === "create") {
const viewFiles = this.getViewFiles(manifestPath);
// for current document, re-collect and re-assign it to avoid cache issue
if (viewFiles[documentPath]) {
const idCollector = new IdsCollectorVisitor(documentPath);
accept(viewFiles[documentPath], idCollector);
this.controlIds[manifestPath][documentPath] =
idCollector.getControlIds();
}
return;
}

const idControls = this.getControlIds(manifestPath);
delete idControls[documentPath];
this.setControlIds(manifestPath, idControls);
}
}

/**
Expand Down
14 changes: 13 additions & 1 deletion packages/context/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import type {
} from "@ui5-language-assistant/semantic-model-types";
import { ConvertedMetadata } from "@sap-ux/vocabularies-types";
import type { Manifest } from "@sap-ux/project-access";
import { FetchResponse } from "@ui5-language-assistant/logic-utils";
import {
FetchResponse,
OffsetRange,
} from "@ui5-language-assistant/logic-utils";
import type { XMLDocument } from "@xml-tools/ast";
import { Location } from "vscode-languageserver";

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

Expand All @@ -18,12 +23,19 @@ export enum DirName {
Ext = "ext",
}

export interface ControlIdLocation extends Location {
offsetRange: OffsetRange;
}

export interface Context {
ui5Model: UI5SemanticModel;
manifestDetails: ManifestDetails;
yamlDetails: YamlDetails;
services: Record<string, ServiceDetails>;
customViewId: string;
viewFiles: Record<string, XMLDocument>;
controlIds: Map<string, ControlIdLocation[]>;
documentPath: string;
}

/**
Expand Down
73 changes: 73 additions & 0 deletions packages/context/src/utils/control-ids.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { cache } from "../api";
import { ControlIdLocation } from "../types";
import { IdsCollectorVisitor } from "./ids-collector";
import { accept } from "@xml-tools/ast";

/**
* Process control ids
*
* @param param parameter object
* @param param.manifestPath path to manifest.json file
* @param param.documentPath path to xml view file
*/
function processControlIds(param: {
manifestPath: string;
documentPath: string;
}): void {
const { documentPath, manifestPath } = param;
// check cache
if (Object.keys(cache.getControlIds(manifestPath)).length > 0) {
// for current document, re-collect and re-assign it to avoid cache issue
cache.setControlIdsForViewFile({
manifestPath,
documentPath,
operation: "create",
});
return;
}

// build fresh
const ctrIds: Record<string, Map<string, ControlIdLocation[]>> = {};
const viewFiles = cache.getViewFiles(manifestPath);
const files = Object.keys(viewFiles);
for (const docPath of files) {
const idCollector = new IdsCollectorVisitor(docPath);
accept(viewFiles[docPath], idCollector);
ctrIds[docPath] = idCollector.getControlIds();
}
cache.setControlIds(manifestPath, ctrIds);
}

/**
* Get control ids of all xml files.
*
* @param param parameter object
* @param param.manifestPath path to manifest.json file
* @param param.documentPath path to xml view file
* @returns merged control ids of all xml files
*/
export function getControlIds(param: {
manifestPath: string;
documentPath: string;
}): Map<string, ControlIdLocation[]> {
const { manifestPath } = param;

processControlIds(param);

const allDocumentsIds = cache.getControlIds(manifestPath);
const keys = Object.keys(allDocumentsIds);

const mergedIds: Map<string, ControlIdLocation[]> = new Map();
for (const doc of keys) {
const ids = allDocumentsIds[doc];
for (const [id, location] of ids) {
const existing = mergedIds.get(id);
if (existing) {
mergedIds.set(id, [...existing, ...location]);
} else {
mergedIds.set(id, location);
}
}
}
return mergedIds;
}
Loading
Loading