Skip to content

Commit

Permalink
feat: Generate unique ID across view files in an application (#719)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
marufrasully authored Aug 21, 2024
1 parent bcd5523 commit d6ceeaa
Show file tree
Hide file tree
Showing 66 changed files with 1,960 additions and 933 deletions.
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
33 changes: 31 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,31 @@ 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,
content,
});
return {
manifestDetails,
yamlDetails,
ui5Model,
services,
customViewId,
viewFiles,
controlIds,
documentPath,
};
} catch (error) {
getLogger().debug("getContext failed:", {
error,
});
return error as Error;
}
}
Expand Down
107 changes: 105 additions & 2 deletions packages/context/src/cache.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
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";
import { FileChangeType } from "vscode-languageserver/node";

type AbsoluteAppRoot = string;
type AbsoluteProjectRoot = string;
Expand All @@ -13,13 +15,20 @@ class Cache {
private CAPServices: Map<AbsoluteProjectRoot, Map<string, string>>;
private ui5YamlDetails: Map<string, YamlDetails>;
private ui5Model: Map<string, UI5SemanticModel>;
private viewFiles: Map<string, Record<string, XMLDocument>>;
private controlIds: Map<
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 = new Map();
this.controlIds = new Map();
}
reset() {
this.project = new Map();
Expand All @@ -28,6 +37,8 @@ class Cache {
this.CAPServices = new Map();
this.ui5YamlDetails = new Map();
this.ui5Model = new Map();
this.viewFiles = new Map();
this.controlIds = new Map();
}
/**
* Get entries of cached project
Expand Down Expand Up @@ -124,6 +135,98 @@ 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.get(manifestPath) ?? {};
}

setViewFiles(
manifestPath: string,
viewFiles: Record<string, XMLDocument>
): void {
this.viewFiles.set(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: Exclude<FileChangeType, 2>;
content?: string;
}): Promise<void> {
const { manifestPath, documentPath, operation, content } = param;
if (operation === FileChangeType.Created) {
const viewFiles = this.getViewFiles(manifestPath);
viewFiles[documentPath] = await createDocumentAst(documentPath, content);
// assign new view files to cache
this.setViewFiles(manifestPath, viewFiles);
return;
}

const viewFiles = this.getViewFiles(manifestPath);
delete viewFiles[documentPath];
// assign new view files to cache
this.setViewFiles(manifestPath, viewFiles);
}
/**
* Get entries of control ids
*/
getControlIds(
manifestPath: string
): Record<string, Map<string, ControlIdLocation[]>> {
return this.controlIds.get(manifestPath) ?? {};
}
setControlIds(
manifestPath: string,
controlIds: Record<string, Map<string, ControlIdLocation[]>>
) {
this.controlIds.set(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: Exclude<FileChangeType, 2>;
}): void {
const { manifestPath, documentPath, operation } = param;

if (operation === FileChangeType.Created) {
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);
const idControls = this.getControlIds(manifestPath);
idControls[documentPath] = idCollector.getControlIds();
// assign new control ids to cache
this.setControlIds(manifestPath, idControls);
}
return;
}

const idControls = this.getControlIds(manifestPath);
delete idControls[documentPath];
// assign new control ids to cache
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
78 changes: 78 additions & 0 deletions packages/context/src/utils/control-ids.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { FileChangeType } from "vscode-languageserver/node";
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;
content?: string;
}): void {
const { documentPath, manifestPath, content } = param;
// check cache
if (Object.keys(cache.getControlIds(manifestPath)).length > 0) {
if (content) {
// for current document, re-collect and re-assign it to avoid cache issue
cache.setControlIdsForViewFile({
manifestPath,
documentPath,
operation: FileChangeType.Created,
});
}
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;
content?: 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

0 comments on commit d6ceeaa

Please sign in to comment.