From c6bc1c0625f3a1cb27b27215567e9659bf61834e Mon Sep 17 00:00:00 2001 From: "GLOBAL\\C5293748" Date: Fri, 17 Nov 2023 16:03:55 +0200 Subject: [PATCH 1/4] feat: fixed bug, context path completion off --- packages/context/src/types.ts | 1 + packages/context/src/ui5-model.ts | 35 +- packages/context/test/unit/ui5-model.test.ts | 18 + .../completion/providers/context-path.ts | 194 ------- .../services/completion/providers/index.ts | 7 +- .../completion/providers/context-path.test.ts | 497 ------------------ 6 files changed, 44 insertions(+), 708 deletions(-) delete mode 100644 packages/fe/src/services/completion/providers/context-path.ts delete mode 100644 packages/fe/test/unit/services/completion/providers/context-path.test.ts diff --git a/packages/context/src/types.ts b/packages/context/src/types.ts index 1f03346e4..c3d08ea16 100644 --- a/packages/context/src/types.ts +++ b/packages/context/src/types.ts @@ -8,6 +8,7 @@ import { FetchResponse } from "@ui5-language-assistant/logic-utils"; export const DEFAULT_UI5_FRAMEWORK = "SAPUI5"; export const DEFAULT_UI5_VERSION = "1.71.49"; +export const UI5_VERSION_S4_PLACEHOLDER = "${sap.ui5.dist.version}"; export const UI5_FRAMEWORK_CDN_BASE_URL = { OpenUI5: "https://sdk.openui5.org/", SAPUI5: "https://ui5.sap.com/", diff --git a/packages/context/src/ui5-model.ts b/packages/context/src/ui5-model.ts index 6fa3567a8..cda59f2b1 100644 --- a/packages/context/src/ui5-model.ts +++ b/packages/context/src/ui5-model.ts @@ -12,7 +12,7 @@ import { Json, TypeNameFix, } from "@ui5-language-assistant/semantic-model"; -import { Fetcher } from "./types"; +import { Fetcher, UI5_VERSION_S4_PLACEHOLDER } from "./types"; import { fetch } from "@ui5-language-assistant/logic-utils"; import { getLibraryAPIJsonUrl, @@ -262,12 +262,16 @@ async function getVersionInfo( let versionInfo = await readFromCache(cacheFilePath); if (versionInfo === undefined) { const url = await getVersionInfoUrl(framework, version); - const response = await fetcher(url); - if (response.ok) { - versionInfo = await response.json(); - writeToCache(cacheFilePath, versionInfo); - } else { - getLogger().error("Could not read version information", { + try { + const response = await fetcher(url); + if (response.ok) { + versionInfo = await response.json(); + writeToCache(cacheFilePath, versionInfo); + } else { + throw new Error(`Version info request has failed (${url})`); + } + } catch (e) { + getLogger().error("Could not read version information. " + e, { url, }); } @@ -361,11 +365,19 @@ export async function negotiateVersionWithFetcher( // try to negotiate version let isFallback = false; let isIncorrectVersion = false; + let useLatestVersion = false; let versions = versionMap[framework]; let adjustedVersion: string = version || DEFAULT_UI5_VERSION; - if (version && !isVersionSupported(version)) { + if (version === UI5_VERSION_S4_PLACEHOLDER) { + useLatestVersion = true; + adjustedVersion = version; + // TODO: insert actual number into the message + getLogger().warn( + `The version specified as minUI5Version in your manifest.json is not supported by Language Assistant, the latest available version is used instead` + ); + } else if (version && !isVersionSupported(version)) { // version is out of support in LA, using default version getLogger().warn( `The version specified as minUI5Version in your manifest.json is not supported by Language Assistant, the fallback version ${DEFAULT_UI5_VERSION} is used instead` @@ -375,7 +387,7 @@ export async function negotiateVersionWithFetcher( } if (!version) { - // no version defined, using default version + // no version defined, using default or latest version getLogger().warn( "No version defined! Please check the minUI5Version in your manifest.json!" ); @@ -388,6 +400,7 @@ export async function negotiateVersionWithFetcher( isIncorrectVersion = true; } } else if ( + useLatestVersion || !(await getVersionInfo( versionInfoJsonFetcher, modelCachePath, @@ -427,7 +440,7 @@ export async function negotiateVersionWithFetcher( } // coerce the version (check for invalid version, which indicates development scenario) const parsedVersion = semver.coerce(adjustedVersion); - if (parsedVersion) { + if (!useLatestVersion && parsedVersion) { if (versions[`${parsedVersion.major}.${parsedVersion.minor}`]) { // lookup for a valid major.minor entry adjustedVersion = @@ -453,7 +466,7 @@ export async function negotiateVersionWithFetcher( isIncorrectVersion = true; } } else { - // development scenario => use latest version + // development scenario or version placeholder in manifest found => use latest version adjustedVersion = versions["latest"].version; isIncorrectVersion = true; } diff --git a/packages/context/test/unit/ui5-model.test.ts b/packages/context/test/unit/ui5-model.test.ts index 919fb74af..70f6fb10c 100644 --- a/packages/context/test/unit/ui5-model.test.ts +++ b/packages/context/test/unit/ui5-model.test.ts @@ -42,6 +42,7 @@ describe("the UI5 language assistant ui5 model", () => { const FRAMEWORK = "SAPUI5"; const OPEN_FRAMEWORK = "OpenUI5"; const VERSION = "1.71.49"; + const UI5_VERSION_S4_PLACEHOLDER = "${sap.ui5.dist.version}"; const NO_CACHE_FOLDER = undefined; function assertSemanticModel(ui5Model: UI5SemanticModel): void { @@ -623,5 +624,22 @@ describe("the UI5 language assistant ui5 model", () => { expect(objNegotiatedVersionWithFetcher.isFallback).toBeFalse(); expect(objNegotiatedVersionWithFetcher.isIncorrectVersion).toBeTrue(); }); + + it("resolve unsupported version placeholder - S/4 generator artifact (should be latest)", async () => { + const objNegotiatedVersionWithFetcher = await negotiateVersionWithFetcher( + async (): Promise => { + return createResponse(true, 200, versionMap[OPEN_FRAMEWORK]); + }, + async (): Promise => { + return createResponse(false, 404); + }, + cachePath, + FRAMEWORK, + UI5_VERSION_S4_PLACEHOLDER + ); + expect(objNegotiatedVersionWithFetcher.version).toEqual("1.105.0"); + expect(objNegotiatedVersionWithFetcher.isFallback).toBeFalse(); + expect(objNegotiatedVersionWithFetcher.isIncorrectVersion).toBeTrue(); + }); }); }); diff --git a/packages/fe/src/services/completion/providers/context-path.ts b/packages/fe/src/services/completion/providers/context-path.ts deleted file mode 100644 index 0cb720167..000000000 --- a/packages/fe/src/services/completion/providers/context-path.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { getUI5PropertyByXMLAttributeKey } from "@ui5-language-assistant/logic-utils"; -import { - getPathConstraintsForControl, - getNextPossibleContextPathTargets, - getRootElements, - resolvePathTarget, -} from "../../../utils"; -import { - AnnotationTargetInXMLAttributeValueCompletion, - SAP_FE_MACROS, -} from "../../../types"; - -import { UI5AttributeValueCompletionOptions } from "./index"; -import { - EntityContainer, - EntitySet, - EntityType, - NavigationProperty, - Singleton, -} from "@sap-ux/vocabularies-types"; -import { getAffectedRange } from "../utils"; -import { AnnotationTargetInXMLAttributeValueTypeName } from "../../../types/completion"; - -type ApplicableMetadataElement = - | EntityContainer - | EntitySet - | EntityType - | Singleton - | NavigationProperty; - -interface CompletionSuggestion { - element: ApplicableMetadataElement; - isLastSegment: boolean; -} - -/** - * Suggests values for macros contextPath - */ -export function contextPathSuggestions({ - element, - attribute, - context, - prefix, -}: UI5AttributeValueCompletionOptions): AnnotationTargetInXMLAttributeValueCompletion[] { - const ui5Property = getUI5PropertyByXMLAttributeKey( - attribute, - context.ui5Model - ); - - if ( - ui5Property?.library === SAP_FE_MACROS && - ui5Property.name === "contextPath" - ) { - const mainServicePath = context.manifestDetails.mainServicePath; - const service = mainServicePath - ? context.services[mainServicePath] - : undefined; - if (!service) { - return []; - } - const metadata = service.convertedMetadata; - const { expectedAnnotations, expectedTypes } = getPathConstraintsForControl( - element.name, - ui5Property - ); - const isPropertyPath = expectedTypes.includes("Property"); - const suggestions: CompletionSuggestion[] = []; - const segments = (attribute.value || "").split("/"); - const precedingSegments = (prefix || "").split("/"); - const completionSegmentIndex = precedingSegments.length - 1; - precedingSegments.pop(); - const completionSegmentOffset = - precedingSegments.join("/").length + (precedingSegments.length ? 1 : 0); - const isAbsolutePath = segments.length > 1 && !segments[0]; - if (!isAbsolutePath && completionSegmentIndex > 0) { - // relative paths are not supported - return []; - } - if (expectedAnnotations.length + expectedTypes.length === 0) { - return []; - } - - const isNextSegmentPossible = ( - currentTarget: EntitySet | EntityType | Singleton | EntityContainer, - milestones: string[] = [] - ): boolean => { - return ( - getNextPossibleContextPathTargets( - service.convertedMetadata, - currentTarget, - { - allowedTerms: expectedAnnotations, - allowedTargets: expectedTypes, - isPropertyPath, - }, - [...milestones, currentTarget.fullyQualifiedName] - ).length > 0 - ); - }; - - if (completionSegmentIndex < 2) { - // completion for root element - const roots = getRootElements( - metadata, - expectedAnnotations, - expectedTypes, - isPropertyPath - ); - suggestions.push( - ...roots.map((root) => ({ - element: root, - isLastSegment: !isNextSegmentPossible(root), - })) - ); - } else { - // completion for navigation property segment - const precedingPath = segments.slice(0, completionSegmentIndex).join("/"); - const { target, isCollection, milestones } = resolvePathTarget( - service.convertedMetadata, - precedingPath - ); - if (!target) { - // target not resolved or path leads to collection - no further segments possible - return []; - } else if (target._type === "Property") { - // no further segments possible after entity property, container is not supported - return []; - } else { - const possibleTargets = getNextPossibleContextPathTargets( - service.convertedMetadata, - target, - { - allowedTerms: expectedAnnotations, - allowedTargets: expectedTypes, - isPropertyPath, - isCollection: isCollection ? false : undefined, - }, - milestones - ); - suggestions.push( - ...possibleTargets.map((t) => { - const entityType = - t._type === "NavigationProperty" ? t.targetType : t.entityType; - return { - element: t, - isLastSegment: !isNextSegmentPossible(entityType, milestones), - }; - }) - ); - } - } - - const sortMap: Record = { - EntityContainer: "Z", - EntityType: "A", - EntitySet: "B", - Singleton: "C", - NavigationProperty: "N", - }; - - const getSuggestionText = (suggestion: CompletionSuggestion): string => { - const isFullyQualifiedName = [ - "EntityContainer", - "EntitySet", - "Singleton", - ].includes(suggestion.element._type); - return `${completionSegmentIndex === 0 ? "/" : ""}${ - isFullyQualifiedName && completionSegmentIndex < 2 - ? suggestion.element.fullyQualifiedName - : suggestion.element.name - }`; - }; - - return suggestions.map((suggestion) => { - const text = getSuggestionText(suggestion); - return { - type: AnnotationTargetInXMLAttributeValueTypeName, - node: { - kind: suggestion.element._type, - name: text, - text, - affectedRange: getAffectedRange( - attribute.syntax.value, - completionSegmentOffset - ), - commitCharacters: suggestion.isLastSegment ? [] : ["/"], - commitCharactersRequired: true, - sortText: sortMap[suggestion.element._type] + text, - }, - }; - }); - } - return []; -} diff --git a/packages/fe/src/services/completion/providers/index.ts b/packages/fe/src/services/completion/providers/index.ts index 0017a582c..3a58ee404 100644 --- a/packages/fe/src/services/completion/providers/index.ts +++ b/packages/fe/src/services/completion/providers/index.ts @@ -4,18 +4,13 @@ import { AttributeValueCompletionOptions, } from "@xml-tools/content-assist"; import { UI5XMLViewAnnotationCompletion } from "../../../types"; -import { contextPathSuggestions } from "./context-path"; import { filterBarAttributeSuggestions } from "./filter-bar"; import { metaPathSuggestions } from "./meta-path"; export const attributeValueProviders: AttributeValueCompletion< UI5XMLViewAnnotationCompletion, Context ->[] = [ - contextPathSuggestions, - metaPathSuggestions, - filterBarAttributeSuggestions, -]; +>[] = [metaPathSuggestions, filterBarAttributeSuggestions]; export type UI5AttributeValueCompletionOptions = AttributeValueCompletionOptions; diff --git a/packages/fe/test/unit/services/completion/providers/context-path.test.ts b/packages/fe/test/unit/services/completion/providers/context-path.test.ts deleted file mode 100644 index 555ac5f55..000000000 --- a/packages/fe/test/unit/services/completion/providers/context-path.test.ts +++ /dev/null @@ -1,497 +0,0 @@ -import { join } from "path"; -import { Context } from "@ui5-language-assistant/context"; -import { CURSOR_ANCHOR } from "@ui5-language-assistant/test-framework"; -import { Settings } from "@ui5-language-assistant/settings"; - -import { - Config, - ProjectName, - ProjectType, - TestFramework, -} from "@ui5-language-assistant/test-framework"; - -import { CompletionItem } from "vscode-languageserver-types"; -import { - completionItemToSnapshot, - getViewCompletionProvider, - ViewCompletionProviderType, -} from "../../utils"; -import * as miscUtils from "../../../../../src/utils/misc"; - -let framework: TestFramework; - -describe("contextPath attribute value completion", () => { - let root: string, uri: string, documentPath: string; - let getCompletionResult: ViewCompletionProviderType; - - const viewFilePathSegments = [ - "app", - "manage_travels", - "webapp", - "ext", - "main", - "Main.view.xml", - ]; - const annoFileSegmentsCDS = ["app", "manage_travels", "annotations.cds"]; - const settings: Settings = { - codeAssist: { - deprecated: false, - experimental: false, - }, - logging: { - level: "off", - }, - trace: { - server: "off", - }, - SplitAttributesOnFormat: true, - }; - - const annotationSnippetCDS = ` - annotate service.Travel with @( - UI.Chart #sample1 :{ - ChartType: #Bar - }, - ); - `; - beforeAll(async function () { - const config: Config = { - projectInfo: { - name: ProjectName.cap, - type: ProjectType.CAP, - npmInstall: true, - deleteBeforeCopy: false, - }, - }; - framework = new TestFramework(config); - - root = framework.getProjectRoot(); - uri = framework.getFileUri(viewFilePathSegments); - documentPath = join( - root, - "app", - "manage_travels", - "webapp", - "ext", - "main", - "Main.view.xml" - ); - - await framework.updateFileContent( - annoFileSegmentsCDS, - annotationSnippetCDS - ); - getCompletionResult = getViewCompletionProvider( - framework, - viewFilePathSegments, - documentPath, - uri, - settings - ); - }, 5 * 60000); - - describe("contextPath completion", () => { - it("first segment completion", async function () { - const result = await getCompletionResult( - `` - ); - expect( - result.map((item) => completionItemToSnapshot(item)) - ).toStrictEqual([ - "label: /TravelService.EntityContainer; text: /TravelService.EntityContainer; kind:19; commit:/; sort:Z", - "label: /TravelService.EntityContainer/Travel; text: /TravelService.EntityContainer/Travel; kind:2; commit:/; sort:B", - "label: /TravelService.EntityContainer/HighestTotal; text: /TravelService.EntityContainer/HighestTotal; kind:2; commit:; sort:B", - "label: /TravelService.EntityContainer/Currencies; text: /TravelService.EntityContainer/Currencies; kind:2; commit:/; sort:B", - "label: /TravelService.EntityContainer/TravelStatus; text: /TravelService.EntityContainer/TravelStatus; kind:2; commit:/; sort:B", - "label: /TravelService.EntityContainer/TravelAgency; text: /TravelService.EntityContainer/TravelAgency; kind:2; commit:/; sort:B", - "label: /TravelService.EntityContainer/Passenger; text: /TravelService.EntityContainer/Passenger; kind:2; commit:/; sort:B", - "label: /TravelService.EntityContainer/Booking; text: /TravelService.EntityContainer/Booking; kind:2; commit:/; sort:B", - "label: /TravelService.EntityContainer/BookedFlights; text: /TravelService.EntityContainer/BookedFlights; kind:2; commit:/; sort:B", - "label: /TravelService.EntityContainer/Countries; text: /TravelService.EntityContainer/Countries; kind:2; commit:/; sort:B", - "label: /TravelService.EntityContainer/BookingStatus; text: /TravelService.EntityContainer/BookingStatus; kind:2; commit:/; sort:B", - "label: /TravelService.EntityContainer/BookingSupplement; text: /TravelService.EntityContainer/BookingSupplement; kind:2; commit:/; sort:B", - "label: /TravelService.EntityContainer/Airline; text: /TravelService.EntityContainer/Airline; kind:2; commit:/; sort:B", - "label: /TravelService.EntityContainer/Flight; text: /TravelService.EntityContainer/Flight; kind:2; commit:/; sort:B", - "label: /TravelService.EntityContainer/Supplement; text: /TravelService.EntityContainer/Supplement; kind:2; commit:/; sort:B", - "label: /TravelService.EntityContainer/FlightConnection; text: /TravelService.EntityContainer/FlightConnection; kind:2; commit:/; sort:B", - "label: /TravelService.EntityContainer/SupplementType; text: /TravelService.EntityContainer/SupplementType; kind:2; commit:/; sort:B", - "label: /TravelService.EntityContainer/Airport; text: /TravelService.EntityContainer/Airport; kind:2; commit:/; sort:B", - "label: /TravelService.EntityContainer/Currencies_texts; text: /TravelService.EntityContainer/Currencies_texts; kind:2; commit:; sort:B", - "label: /TravelService.EntityContainer/TravelStatus_texts; text: /TravelService.EntityContainer/TravelStatus_texts; kind:2; commit:; sort:B", - "label: /TravelService.EntityContainer/Countries_texts; text: /TravelService.EntityContainer/Countries_texts; kind:2; commit:; sort:B", - "label: /TravelService.EntityContainer/BookingStatus_texts; text: /TravelService.EntityContainer/BookingStatus_texts; kind:2; commit:; sort:B", - "label: /TravelService.EntityContainer/Supplement_texts; text: /TravelService.EntityContainer/Supplement_texts; kind:2; commit:; sort:B", - "label: /TravelService.EntityContainer/SupplementType_texts; text: /TravelService.EntityContainer/SupplementType_texts; kind:2; commit:; sort:B", - "label: /Travel; text: /Travel; kind:7; commit:/; sort:A", - "label: /HighestTotal; text: /HighestTotal; kind:7; commit:; sort:A", - "label: /Currencies; text: /Currencies; kind:7; commit:/; sort:A", - "label: /TravelStatus; text: /TravelStatus; kind:7; commit:/; sort:A", - "label: /TravelAgency; text: /TravelAgency; kind:7; commit:/; sort:A", - "label: /Passenger; text: /Passenger; kind:7; commit:/; sort:A", - "label: /Booking; text: /Booking; kind:7; commit:/; sort:A", - "label: /BookedFlights; text: /BookedFlights; kind:7; commit:/; sort:A", - "label: /Countries; text: /Countries; kind:7; commit:/; sort:A", - "label: /BookingStatus; text: /BookingStatus; kind:7; commit:/; sort:A", - "label: /BookingSupplement; text: /BookingSupplement; kind:7; commit:/; sort:A", - "label: /Airline; text: /Airline; kind:7; commit:/; sort:A", - "label: /Flight; text: /Flight; kind:7; commit:/; sort:A", - "label: /Supplement; text: /Supplement; kind:7; commit:/; sort:A", - "label: /FlightConnection; text: /FlightConnection; kind:7; commit:/; sort:A", - "label: /SupplementType; text: /SupplementType; kind:7; commit:/; sort:A", - "label: /Airport; text: /Airport; kind:7; commit:/; sort:A", - "label: /DraftAdministrativeData; text: /DraftAdministrativeData; kind:7; commit:; sort:A", - "label: /Currencies_texts; text: /Currencies_texts; kind:7; commit:; sort:A", - "label: /TravelStatus_texts; text: /TravelStatus_texts; kind:7; commit:; sort:A", - "label: /Countries_texts; text: /Countries_texts; kind:7; commit:; sort:A", - "label: /BookingStatus_texts; text: /BookingStatus_texts; kind:7; commit:; sort:A", - "label: /Supplement_texts; text: /Supplement_texts; kind:7; commit:; sort:A", - "label: /SupplementType_texts; text: /SupplementType_texts; kind:7; commit:; sort:A", - ]); - }); - - it("first segment completion after slash", async function () { - const result = await getCompletionResult( - `` - ); - expect(result.map((item) => item.insertText)).toStrictEqual([ - "TravelService.EntityContainer", - "TravelService.EntityContainer/Travel", - "TravelService.EntityContainer/HighestTotal", - "TravelService.EntityContainer/Currencies", - "TravelService.EntityContainer/TravelStatus", - "TravelService.EntityContainer/TravelAgency", - "TravelService.EntityContainer/Passenger", - "TravelService.EntityContainer/Booking", - "TravelService.EntityContainer/BookedFlights", - "TravelService.EntityContainer/Countries", - "TravelService.EntityContainer/BookingStatus", - "TravelService.EntityContainer/BookingSupplement", - "TravelService.EntityContainer/Airline", - "TravelService.EntityContainer/Flight", - "TravelService.EntityContainer/Supplement", - "TravelService.EntityContainer/FlightConnection", - "TravelService.EntityContainer/SupplementType", - "TravelService.EntityContainer/Airport", - "TravelService.EntityContainer/Currencies_texts", - "TravelService.EntityContainer/TravelStatus_texts", - "TravelService.EntityContainer/Countries_texts", - "TravelService.EntityContainer/BookingStatus_texts", - "TravelService.EntityContainer/Supplement_texts", - "TravelService.EntityContainer/SupplementType_texts", - "Travel", - "HighestTotal", - "Currencies", - "TravelStatus", - "TravelAgency", - "Passenger", - "Booking", - "BookedFlights", - "Countries", - "BookingStatus", - "BookingSupplement", - "Airline", - "Flight", - "Supplement", - "FlightConnection", - "SupplementType", - "Airport", - "DraftAdministrativeData", - "Currencies_texts", - "TravelStatus_texts", - "Countries_texts", - "BookingStatus_texts", - "Supplement_texts", - "SupplementType_texts", - ]); - }); - - it("entity set completion after container", async function () { - const result = await getCompletionResult( - `` - ); - expect( - result.map((item) => completionItemToSnapshot(item)) - ).toStrictEqual([ - "label: Travel; text: Travel; kind:2; commit:/; sort:B", - "label: Currencies; text: Currencies; kind:2; commit:/; sort:B", - "label: TravelStatus; text: TravelStatus; kind:2; commit:/; sort:B", - "label: TravelAgency; text: TravelAgency; kind:2; commit:/; sort:B", - "label: Passenger; text: Passenger; kind:2; commit:/; sort:B", - "label: Booking; text: Booking; kind:2; commit:/; sort:B", - "label: BookedFlights; text: BookedFlights; kind:2; commit:/; sort:B", - "label: Countries; text: Countries; kind:2; commit:/; sort:B", - "label: BookingStatus; text: BookingStatus; kind:2; commit:/; sort:B", - "label: BookingSupplement; text: BookingSupplement; kind:2; commit:/; sort:B", - "label: Airline; text: Airline; kind:2; commit:/; sort:B", - "label: Flight; text: Flight; kind:2; commit:/; sort:B", - "label: Supplement; text: Supplement; kind:2; commit:/; sort:B", - "label: FlightConnection; text: FlightConnection; kind:2; commit:/; sort:B", - "label: SupplementType; text: SupplementType; kind:2; commit:/; sort:B", - "label: Airport; text: Airport; kind:2; commit:/; sort:B", - ]); - }); - - it("navigation segment completion", async function () { - const result = await getCompletionResult( - `` - ); - expect( - result.map((item) => completionItemToSnapshot(item)) - ).toStrictEqual([ - "label: CurrencyCode; text: CurrencyCode; kind:18; commit:/; sort:N", - "label: BookingStatus; text: BookingStatus; kind:18; commit:/; sort:N", - "label: to_BookSupplement; text: to_BookSupplement; kind:18; commit:/; sort:N", - "label: to_Carrier; text: to_Carrier; kind:18; commit:/; sort:N", - "label: to_Customer; text: to_Customer; kind:18; commit:/; sort:N", - "label: to_Travel; text: to_Travel; kind:18; commit:/; sort:N", - "label: to_Flight; text: to_Flight; kind:18; commit:/; sort:N", - "label: DraftAdministrativeData; text: DraftAdministrativeData; kind:18; commit:; sort:N", - ]); - }); - it("navigation segment completion (case 2)", async function () { - const result = await getCompletionResult( - `` - ); - expect( - result.map((item) => completionItemToSnapshot(item)) - ).toStrictEqual([ - "label: CurrencyCode; text: CurrencyCode; kind:18; commit:/; sort:N", - "label: BookingStatus; text: BookingStatus; kind:18; commit:/; sort:N", - "label: to_BookSupplement; text: to_BookSupplement; kind:18; commit:/; sort:N", - "label: to_Carrier; text: to_Carrier; kind:18; commit:/; sort:N", - "label: to_Customer; text: to_Customer; kind:18; commit:/; sort:N", - "label: to_Travel; text: to_Travel; kind:18; commit:/; sort:N", - "label: to_Flight; text: to_Flight; kind:18; commit:/; sort:N", - "label: DraftAdministrativeData; text: DraftAdministrativeData; kind:18; commit:; sort:N", - ]); - }); - - it("navigation segment completion leading to entity type the same as entity set type", async function () { - const result = await getCompletionResult( - `` - ); - expect( - result.map((item) => completionItemToSnapshot(item)) - ).toStrictEqual([ - "label: CurrencyCode; text: CurrencyCode; kind:18; commit:/; sort:N", - "label: BookingStatus; text: BookingStatus; kind:18; commit:/; sort:N", - "label: to_Carrier; text: to_Carrier; kind:18; commit:/; sort:N", - "label: to_Customer; text: to_Customer; kind:18; commit:/; sort:N", - "label: to_Travel; text: to_Travel; kind:18; commit:/; sort:N", - "label: to_Flight; text: to_Flight; kind:18; commit:/; sort:N", - "label: DraftAdministrativeData; text: DraftAdministrativeData; kind:18; commit:; sort:N", - ]); - }); - - it("navigation segment completion - no cyclic routes", async function () { - const result = await getCompletionResult( - `` - ); - expect( - result.map((item) => completionItemToSnapshot(item)) - ).toStrictEqual([ - "label: CurrencyCode; text: CurrencyCode; kind:18; commit:/; sort:N", - "label: BookingStatus; text: BookingStatus; kind:18; commit:/; sort:N", - "label: to_Carrier; text: to_Carrier; kind:18; commit:/; sort:N", - "label: to_Customer; text: to_Customer; kind:18; commit:/; sort:N", - "label: to_Flight; text: to_Flight; kind:18; commit:/; sort:N", - "label: DraftAdministrativeData; text: DraftAdministrativeData; kind:18; commit:; sort:N", - ]); - }); - - describe("when contextPath spec contains expected terms", () => { - let constraintsStub: jest.SpyInstance; - beforeAll(() => { - constraintsStub = jest - .spyOn(miscUtils, "getPathConstraintsForControl") - .mockReturnValue({ - expectedAnnotations: [ - { - alias: "UI", - fullyQualifiedName: "com.sap.vocabularies.UI.v1.Chart", - name: "Chart", - }, - ], - expectedTypes: ["EntitySet", "EntityType"], - }); - }); - afterAll(() => { - constraintsStub.mockRestore(); - }); - - it("first segment completion", async function () { - const result = await getCompletionResult( - `` - ); - expect( - result.map((item) => completionItemToSnapshot(item)) - ).toStrictEqual([ - "label: /TravelService.EntityContainer; text: /TravelService.EntityContainer; kind:19; commit:/; sort:Z", - "label: /TravelService.EntityContainer/Travel; text: /TravelService.EntityContainer/Travel; kind:2; commit:/; sort:B", - "label: /TravelService.EntityContainer/Booking; text: /TravelService.EntityContainer/Booking; kind:2; commit:/; sort:B", - "label: /TravelService.EntityContainer/BookedFlights; text: /TravelService.EntityContainer/BookedFlights; kind:2; commit:/; sort:B", - "label: /TravelService.EntityContainer/BookingSupplement; text: /TravelService.EntityContainer/BookingSupplement; kind:2; commit:/; sort:B", - "label: /Travel; text: /Travel; kind:7; commit:; sort:A", - "label: /Booking; text: /Booking; kind:7; commit:/; sort:A", - "label: /BookedFlights; text: /BookedFlights; kind:7; commit:/; sort:A", - "label: /BookingSupplement; text: /BookingSupplement; kind:7; commit:/; sort:A", - ]); - }); - - it("first segment completion after slash", async function () { - const result = await getCompletionResult( - `` - ); - expect(result.map((item) => item.insertText)).toStrictEqual([ - "TravelService.EntityContainer", - "TravelService.EntityContainer/Travel", - "TravelService.EntityContainer/Booking", - "TravelService.EntityContainer/BookedFlights", - "TravelService.EntityContainer/BookingSupplement", - "Travel", - "Booking", - "BookedFlights", - "BookingSupplement", - ]); - }); - - it("entity set completion after container", async function () { - const result = await getCompletionResult( - `` - ); - expect( - result.map((item) => completionItemToSnapshot(item)) - ).toStrictEqual([ - "label: Travel; text: Travel; kind:2; commit:; sort:B", - "label: Booking; text: Booking; kind:2; commit:/; sort:B", - "label: BookedFlights; text: BookedFlights; kind:2; commit:/; sort:B", - "label: BookingSupplement; text: BookingSupplement; kind:2; commit:/; sort:B", - ]); - }); - - it("navigation segment completion", async function () { - const result = await getCompletionResult( - `` - ); - expect( - result.map((item) => completionItemToSnapshot(item)) - ).toStrictEqual([ - "label: to_BookSupplement; text: to_BookSupplement; kind:18; commit:/; sort:N", - "label: to_Travel; text: to_Travel; kind:18; commit:; sort:N", - ]); - }); - - it("navigation segment completion (case 2)", async function () { - const result = await getCompletionResult( - `` - ); - expect( - result.map((item) => completionItemToSnapshot(item)) - ).toStrictEqual([ - "label: to_BookSupplement; text: to_BookSupplement; kind:18; commit:/; sort:N", - "label: to_Travel; text: to_Travel; kind:18; commit:; sort:N", - ]); - }); - - it("navigation segment completion leading to entity type the same as entity set type", async function () { - const result = await getCompletionResult( - `` - ); - expect( - result.map((item) => completionItemToSnapshot(item)) - ).toStrictEqual([ - "label: to_Travel; text: to_Travel; kind:18; commit:; sort:N", - ]); - }); - - it("navigation segment completion - no cyclic routes", async function () { - const result = await getCompletionResult( - `` - ); - expect( - result.map((item) => completionItemToSnapshot(item)) - ).toStrictEqual([]); - }); - }); - - describe("no completion when...", () => { - it("UI5Property not found", async function () { - const result = await getCompletionResult( - ``, - (c) => { - const newClasses = { ...c.ui5Model.classes }; - delete newClasses["sap.fe.macros.Chart"]; - const newContext: Context = { - ...c, - ui5Model: { - ...c.ui5Model, - classes: newClasses, - }, - }; - return newContext; - } - ); - expect(result.length).toEqual(0); - }); - - it("services unavailable", async function () { - const result = await getCompletionResult( - ``, - (c) => ({ ...c, services: {} }) - ); - expect(result.length).toEqual(0); - }); - - it("service path is not provided in manifest", async function () { - const result = await getCompletionResult( - ``, - - (c) => ({ - ...c, - manifestDetails: { - ...c.manifestDetails, - mainServicePath: "", - }, - }) - ); - expect(result.length).toEqual(0); - }); - - it("path is relative", async function () { - const result = await getCompletionResult( - `` - ); - expect(result.length).toEqual(0); - }); - - it("empty metadata", async function () { - const constraintsStub = jest - .spyOn(miscUtils, "getPathConstraintsForControl") - .mockReturnValue({ expectedAnnotations: [], expectedTypes: [] }); - let result: CompletionItem[]; - try { - result = await getCompletionResult( - `` - ); - } finally { - constraintsStub.mockRestore(); - } - expect(result.length).toEqual(0); - }); - - it("existing path target is not resolved", async function () { - const result = await getCompletionResult( - `` - ); - expect(result.length).toEqual(0); - }); - - it("existing path target is a property", async function () { - const result = await getCompletionResult( - `` - ); - expect(result.length).toEqual(0); - }); - }); - }); -}); From 87516fda93b37fc7530a8dcda0f67e3d8cf93607 Mon Sep 17 00:00:00 2001 From: "GLOBAL\\C5293748" Date: Fri, 17 Nov 2023 16:07:49 +0200 Subject: [PATCH 2/4] fix: changeset added --- .changeset/curly-beers-compete.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/curly-beers-compete.md diff --git a/.changeset/curly-beers-compete.md b/.changeset/curly-beers-compete.md new file mode 100644 index 000000000..18c4c306a --- /dev/null +++ b/.changeset/curly-beers-compete.md @@ -0,0 +1,8 @@ +--- +"@ui5-language-assistant/context": patch +"@ui5-language-assistant/fe": patch +"vscode-ui5-language-assistant": patch +"@ui5-language-assistant/vscode-ui5-language-assistant-bas-ext": patch +--- + +Code completion for target path is disabled, S/4 version placeholder in manifest's minUI5Version property defaults to the latest available version From 44cfd342093245a93c84dc7e287581a5b9cb992e Mon Sep 17 00:00:00 2001 From: "GLOBAL\\C5293748" Date: Fri, 17 Nov 2023 20:51:27 +0200 Subject: [PATCH 3/4] fix: reverted deleted files, test adaptation --- .../completion/providers/context-path.ts | 200 +++++++ .../services/completion/providers/index.ts | 7 +- .../completion/providers/context-path.test.ts | 512 ++++++++++++++++++ 3 files changed, 718 insertions(+), 1 deletion(-) create mode 100644 packages/fe/src/services/completion/providers/context-path.ts create mode 100644 packages/fe/test/unit/services/completion/providers/context-path.test.ts diff --git a/packages/fe/src/services/completion/providers/context-path.ts b/packages/fe/src/services/completion/providers/context-path.ts new file mode 100644 index 000000000..17228443a --- /dev/null +++ b/packages/fe/src/services/completion/providers/context-path.ts @@ -0,0 +1,200 @@ +import { getUI5PropertyByXMLAttributeKey } from "@ui5-language-assistant/logic-utils"; +import { + getPathConstraintsForControl, + getNextPossibleContextPathTargets, + getRootElements, + resolvePathTarget, +} from "../../../utils"; +import { + AnnotationTargetInXMLAttributeValueCompletion, + SAP_FE_MACROS, +} from "../../../types"; + +import { UI5AttributeValueCompletionOptions } from "./index"; +import { + EntityContainer, + EntitySet, + EntityType, + NavigationProperty, + Singleton, +} from "@sap-ux/vocabularies-types"; +import { getAffectedRange } from "../utils"; +import { AnnotationTargetInXMLAttributeValueTypeName } from "../../../types/completion"; + +type ApplicableMetadataElement = + | EntityContainer + | EntitySet + | EntityType + | Singleton + | NavigationProperty; + +interface CompletionSuggestion { + element: ApplicableMetadataElement; + isLastSegment: boolean; +} + +/** + * Suggests values for macros contextPath + */ +export function contextPathSuggestions({ + element, + attribute, + context, + prefix, +}: UI5AttributeValueCompletionOptions): AnnotationTargetInXMLAttributeValueCompletion[] { + const ui5Property = getUI5PropertyByXMLAttributeKey( + attribute, + context.ui5Model + ); + + // provider is blocked and is used in tests only + // reserved for the future to be reused in binding expressions + if (!(context as unknown as { forTest: boolean }).forTest) { + return []; + } + + if ( + ui5Property?.library === SAP_FE_MACROS && + ui5Property.name === "contextPath" + ) { + const mainServicePath = context.manifestDetails.mainServicePath; + const service = mainServicePath + ? context.services[mainServicePath] + : undefined; + if (!service) { + return []; + } + const metadata = service.convertedMetadata; + const { expectedAnnotations, expectedTypes } = getPathConstraintsForControl( + element.name, + ui5Property + ); + const isPropertyPath = expectedTypes.includes("Property"); + const suggestions: CompletionSuggestion[] = []; + const segments = (attribute.value || "").split("/"); + const precedingSegments = (prefix || "").split("/"); + const completionSegmentIndex = precedingSegments.length - 1; + precedingSegments.pop(); + const completionSegmentOffset = + precedingSegments.join("/").length + (precedingSegments.length ? 1 : 0); + const isAbsolutePath = segments.length > 1 && !segments[0]; + if (!isAbsolutePath && completionSegmentIndex > 0) { + // relative paths are not supported + return []; + } + if (expectedAnnotations.length + expectedTypes.length === 0) { + return []; + } + + const isNextSegmentPossible = ( + currentTarget: EntitySet | EntityType | Singleton | EntityContainer, + milestones: string[] = [] + ): boolean => { + return ( + getNextPossibleContextPathTargets( + service.convertedMetadata, + currentTarget, + { + allowedTerms: expectedAnnotations, + allowedTargets: expectedTypes, + isPropertyPath, + }, + [...milestones, currentTarget.fullyQualifiedName] + ).length > 0 + ); + }; + + if (completionSegmentIndex < 2) { + // completion for root element + const roots = getRootElements( + metadata, + expectedAnnotations, + expectedTypes, + isPropertyPath + ); + suggestions.push( + ...roots.map((root) => ({ + element: root, + isLastSegment: !isNextSegmentPossible(root), + })) + ); + } else { + // completion for navigation property segment + const precedingPath = segments.slice(0, completionSegmentIndex).join("/"); + const { target, isCollection, milestones } = resolvePathTarget( + service.convertedMetadata, + precedingPath + ); + if (!target) { + // target not resolved or path leads to collection - no further segments possible + return []; + } else if (target._type === "Property") { + // no further segments possible after entity property, container is not supported + return []; + } else { + const possibleTargets = getNextPossibleContextPathTargets( + service.convertedMetadata, + target, + { + allowedTerms: expectedAnnotations, + allowedTargets: expectedTypes, + isPropertyPath, + isCollection: isCollection ? false : undefined, + }, + milestones + ); + suggestions.push( + ...possibleTargets.map((t) => { + const entityType = + t._type === "NavigationProperty" ? t.targetType : t.entityType; + return { + element: t, + isLastSegment: !isNextSegmentPossible(entityType, milestones), + }; + }) + ); + } + } + + const sortMap: Record = { + EntityContainer: "Z", + EntityType: "A", + EntitySet: "B", + Singleton: "C", + NavigationProperty: "N", + }; + + const getSuggestionText = (suggestion: CompletionSuggestion): string => { + const isFullyQualifiedName = [ + "EntityContainer", + "EntitySet", + "Singleton", + ].includes(suggestion.element._type); + return `${completionSegmentIndex === 0 ? "/" : ""}${ + isFullyQualifiedName && completionSegmentIndex < 2 + ? suggestion.element.fullyQualifiedName + : suggestion.element.name + }`; + }; + + return suggestions.map((suggestion) => { + const text = getSuggestionText(suggestion); + return { + type: AnnotationTargetInXMLAttributeValueTypeName, + node: { + kind: suggestion.element._type, + name: text, + text, + affectedRange: getAffectedRange( + attribute.syntax.value, + completionSegmentOffset + ), + commitCharacters: suggestion.isLastSegment ? [] : ["/"], + commitCharactersRequired: true, + sortText: sortMap[suggestion.element._type] + text, + }, + }; + }); + } + return []; +} diff --git a/packages/fe/src/services/completion/providers/index.ts b/packages/fe/src/services/completion/providers/index.ts index 3a58ee404..0017a582c 100644 --- a/packages/fe/src/services/completion/providers/index.ts +++ b/packages/fe/src/services/completion/providers/index.ts @@ -4,13 +4,18 @@ import { AttributeValueCompletionOptions, } from "@xml-tools/content-assist"; import { UI5XMLViewAnnotationCompletion } from "../../../types"; +import { contextPathSuggestions } from "./context-path"; import { filterBarAttributeSuggestions } from "./filter-bar"; import { metaPathSuggestions } from "./meta-path"; export const attributeValueProviders: AttributeValueCompletion< UI5XMLViewAnnotationCompletion, Context ->[] = [metaPathSuggestions, filterBarAttributeSuggestions]; +>[] = [ + contextPathSuggestions, + metaPathSuggestions, + filterBarAttributeSuggestions, +]; export type UI5AttributeValueCompletionOptions = AttributeValueCompletionOptions; diff --git a/packages/fe/test/unit/services/completion/providers/context-path.test.ts b/packages/fe/test/unit/services/completion/providers/context-path.test.ts new file mode 100644 index 000000000..599495ab4 --- /dev/null +++ b/packages/fe/test/unit/services/completion/providers/context-path.test.ts @@ -0,0 +1,512 @@ +import { join } from "path"; +import { Context } from "@ui5-language-assistant/context"; +import { CURSOR_ANCHOR } from "@ui5-language-assistant/test-framework"; +import { Settings } from "@ui5-language-assistant/settings"; + +import { + Config, + ProjectName, + ProjectType, + TestFramework, +} from "@ui5-language-assistant/test-framework"; + +import { CompletionItem } from "vscode-languageserver-types"; +import { + completionItemToSnapshot, + getViewCompletionProvider, + ViewCompletionProviderType, +} from "../../utils"; +import * as miscUtils from "../../../../../src/utils/misc"; + +let framework: TestFramework; + +describe("contextPath attribute value completion", () => { + let root: string, uri: string, documentPath: string; + let getCompletionResult: ViewCompletionProviderType; + + const viewFilePathSegments = [ + "app", + "manage_travels", + "webapp", + "ext", + "main", + "Main.view.xml", + ]; + const annoFileSegmentsCDS = ["app", "manage_travels", "annotations.cds"]; + const settings: Settings = { + codeAssist: { + deprecated: false, + experimental: false, + }, + logging: { + level: "off", + }, + trace: { + server: "off", + }, + SplitAttributesOnFormat: true, + }; + + const annotationSnippetCDS = ` + annotate service.Travel with @( + UI.Chart #sample1 :{ + ChartType: #Bar + }, + ); + `; + beforeAll(async function () { + const config: Config = { + projectInfo: { + name: ProjectName.cap, + type: ProjectType.CAP, + npmInstall: true, + deleteBeforeCopy: false, + }, + }; + framework = new TestFramework(config); + + root = framework.getProjectRoot(); + uri = framework.getFileUri(viewFilePathSegments); + documentPath = join( + root, + "app", + "manage_travels", + "webapp", + "ext", + "main", + "Main.view.xml" + ); + + await framework.updateFileContent( + annoFileSegmentsCDS, + annotationSnippetCDS + ); + + const provider = getViewCompletionProvider( + framework, + viewFilePathSegments, + documentPath, + uri, + settings + ); + + getCompletionResult = ( + snippet: string, + contextAdapter?: (context: Context) => Context + ) => { + const testAdapter = (context: Context) => { + const result: Context = contextAdapter + ? contextAdapter(context) + : context; + return { ...result, forTest: true } as Context; + }; + + return provider(snippet, testAdapter); + }; + }, 5 * 60000); + + describe("contextPath completion", () => { + it("first segment completion", async function () { + const result = await getCompletionResult( + `` + ); + expect( + result.map((item) => completionItemToSnapshot(item)) + ).toStrictEqual([ + "label: /TravelService.EntityContainer; text: /TravelService.EntityContainer; kind:19; commit:/; sort:Z", + "label: /TravelService.EntityContainer/Travel; text: /TravelService.EntityContainer/Travel; kind:2; commit:/; sort:B", + "label: /TravelService.EntityContainer/HighestTotal; text: /TravelService.EntityContainer/HighestTotal; kind:2; commit:; sort:B", + "label: /TravelService.EntityContainer/Currencies; text: /TravelService.EntityContainer/Currencies; kind:2; commit:/; sort:B", + "label: /TravelService.EntityContainer/TravelStatus; text: /TravelService.EntityContainer/TravelStatus; kind:2; commit:/; sort:B", + "label: /TravelService.EntityContainer/TravelAgency; text: /TravelService.EntityContainer/TravelAgency; kind:2; commit:/; sort:B", + "label: /TravelService.EntityContainer/Passenger; text: /TravelService.EntityContainer/Passenger; kind:2; commit:/; sort:B", + "label: /TravelService.EntityContainer/Booking; text: /TravelService.EntityContainer/Booking; kind:2; commit:/; sort:B", + "label: /TravelService.EntityContainer/BookedFlights; text: /TravelService.EntityContainer/BookedFlights; kind:2; commit:/; sort:B", + "label: /TravelService.EntityContainer/Countries; text: /TravelService.EntityContainer/Countries; kind:2; commit:/; sort:B", + "label: /TravelService.EntityContainer/BookingStatus; text: /TravelService.EntityContainer/BookingStatus; kind:2; commit:/; sort:B", + "label: /TravelService.EntityContainer/BookingSupplement; text: /TravelService.EntityContainer/BookingSupplement; kind:2; commit:/; sort:B", + "label: /TravelService.EntityContainer/Airline; text: /TravelService.EntityContainer/Airline; kind:2; commit:/; sort:B", + "label: /TravelService.EntityContainer/Flight; text: /TravelService.EntityContainer/Flight; kind:2; commit:/; sort:B", + "label: /TravelService.EntityContainer/Supplement; text: /TravelService.EntityContainer/Supplement; kind:2; commit:/; sort:B", + "label: /TravelService.EntityContainer/FlightConnection; text: /TravelService.EntityContainer/FlightConnection; kind:2; commit:/; sort:B", + "label: /TravelService.EntityContainer/SupplementType; text: /TravelService.EntityContainer/SupplementType; kind:2; commit:/; sort:B", + "label: /TravelService.EntityContainer/Airport; text: /TravelService.EntityContainer/Airport; kind:2; commit:/; sort:B", + "label: /TravelService.EntityContainer/Currencies_texts; text: /TravelService.EntityContainer/Currencies_texts; kind:2; commit:; sort:B", + "label: /TravelService.EntityContainer/TravelStatus_texts; text: /TravelService.EntityContainer/TravelStatus_texts; kind:2; commit:; sort:B", + "label: /TravelService.EntityContainer/Countries_texts; text: /TravelService.EntityContainer/Countries_texts; kind:2; commit:; sort:B", + "label: /TravelService.EntityContainer/BookingStatus_texts; text: /TravelService.EntityContainer/BookingStatus_texts; kind:2; commit:; sort:B", + "label: /TravelService.EntityContainer/Supplement_texts; text: /TravelService.EntityContainer/Supplement_texts; kind:2; commit:; sort:B", + "label: /TravelService.EntityContainer/SupplementType_texts; text: /TravelService.EntityContainer/SupplementType_texts; kind:2; commit:; sort:B", + "label: /Travel; text: /Travel; kind:7; commit:/; sort:A", + "label: /HighestTotal; text: /HighestTotal; kind:7; commit:; sort:A", + "label: /Currencies; text: /Currencies; kind:7; commit:/; sort:A", + "label: /TravelStatus; text: /TravelStatus; kind:7; commit:/; sort:A", + "label: /TravelAgency; text: /TravelAgency; kind:7; commit:/; sort:A", + "label: /Passenger; text: /Passenger; kind:7; commit:/; sort:A", + "label: /Booking; text: /Booking; kind:7; commit:/; sort:A", + "label: /BookedFlights; text: /BookedFlights; kind:7; commit:/; sort:A", + "label: /Countries; text: /Countries; kind:7; commit:/; sort:A", + "label: /BookingStatus; text: /BookingStatus; kind:7; commit:/; sort:A", + "label: /BookingSupplement; text: /BookingSupplement; kind:7; commit:/; sort:A", + "label: /Airline; text: /Airline; kind:7; commit:/; sort:A", + "label: /Flight; text: /Flight; kind:7; commit:/; sort:A", + "label: /Supplement; text: /Supplement; kind:7; commit:/; sort:A", + "label: /FlightConnection; text: /FlightConnection; kind:7; commit:/; sort:A", + "label: /SupplementType; text: /SupplementType; kind:7; commit:/; sort:A", + "label: /Airport; text: /Airport; kind:7; commit:/; sort:A", + "label: /DraftAdministrativeData; text: /DraftAdministrativeData; kind:7; commit:; sort:A", + "label: /Currencies_texts; text: /Currencies_texts; kind:7; commit:; sort:A", + "label: /TravelStatus_texts; text: /TravelStatus_texts; kind:7; commit:; sort:A", + "label: /Countries_texts; text: /Countries_texts; kind:7; commit:; sort:A", + "label: /BookingStatus_texts; text: /BookingStatus_texts; kind:7; commit:; sort:A", + "label: /Supplement_texts; text: /Supplement_texts; kind:7; commit:; sort:A", + "label: /SupplementType_texts; text: /SupplementType_texts; kind:7; commit:; sort:A", + ]); + }); + + it("first segment completion after slash", async function () { + const result = await getCompletionResult( + `` + ); + expect(result.map((item) => item.insertText)).toStrictEqual([ + "TravelService.EntityContainer", + "TravelService.EntityContainer/Travel", + "TravelService.EntityContainer/HighestTotal", + "TravelService.EntityContainer/Currencies", + "TravelService.EntityContainer/TravelStatus", + "TravelService.EntityContainer/TravelAgency", + "TravelService.EntityContainer/Passenger", + "TravelService.EntityContainer/Booking", + "TravelService.EntityContainer/BookedFlights", + "TravelService.EntityContainer/Countries", + "TravelService.EntityContainer/BookingStatus", + "TravelService.EntityContainer/BookingSupplement", + "TravelService.EntityContainer/Airline", + "TravelService.EntityContainer/Flight", + "TravelService.EntityContainer/Supplement", + "TravelService.EntityContainer/FlightConnection", + "TravelService.EntityContainer/SupplementType", + "TravelService.EntityContainer/Airport", + "TravelService.EntityContainer/Currencies_texts", + "TravelService.EntityContainer/TravelStatus_texts", + "TravelService.EntityContainer/Countries_texts", + "TravelService.EntityContainer/BookingStatus_texts", + "TravelService.EntityContainer/Supplement_texts", + "TravelService.EntityContainer/SupplementType_texts", + "Travel", + "HighestTotal", + "Currencies", + "TravelStatus", + "TravelAgency", + "Passenger", + "Booking", + "BookedFlights", + "Countries", + "BookingStatus", + "BookingSupplement", + "Airline", + "Flight", + "Supplement", + "FlightConnection", + "SupplementType", + "Airport", + "DraftAdministrativeData", + "Currencies_texts", + "TravelStatus_texts", + "Countries_texts", + "BookingStatus_texts", + "Supplement_texts", + "SupplementType_texts", + ]); + }); + + it("entity set completion after container", async function () { + const result = await getCompletionResult( + `` + ); + expect( + result.map((item) => completionItemToSnapshot(item)) + ).toStrictEqual([ + "label: Travel; text: Travel; kind:2; commit:/; sort:B", + "label: Currencies; text: Currencies; kind:2; commit:/; sort:B", + "label: TravelStatus; text: TravelStatus; kind:2; commit:/; sort:B", + "label: TravelAgency; text: TravelAgency; kind:2; commit:/; sort:B", + "label: Passenger; text: Passenger; kind:2; commit:/; sort:B", + "label: Booking; text: Booking; kind:2; commit:/; sort:B", + "label: BookedFlights; text: BookedFlights; kind:2; commit:/; sort:B", + "label: Countries; text: Countries; kind:2; commit:/; sort:B", + "label: BookingStatus; text: BookingStatus; kind:2; commit:/; sort:B", + "label: BookingSupplement; text: BookingSupplement; kind:2; commit:/; sort:B", + "label: Airline; text: Airline; kind:2; commit:/; sort:B", + "label: Flight; text: Flight; kind:2; commit:/; sort:B", + "label: Supplement; text: Supplement; kind:2; commit:/; sort:B", + "label: FlightConnection; text: FlightConnection; kind:2; commit:/; sort:B", + "label: SupplementType; text: SupplementType; kind:2; commit:/; sort:B", + "label: Airport; text: Airport; kind:2; commit:/; sort:B", + ]); + }); + + it("navigation segment completion", async function () { + const result = await getCompletionResult( + `` + ); + expect( + result.map((item) => completionItemToSnapshot(item)) + ).toStrictEqual([ + "label: CurrencyCode; text: CurrencyCode; kind:18; commit:/; sort:N", + "label: BookingStatus; text: BookingStatus; kind:18; commit:/; sort:N", + "label: to_BookSupplement; text: to_BookSupplement; kind:18; commit:/; sort:N", + "label: to_Carrier; text: to_Carrier; kind:18; commit:/; sort:N", + "label: to_Customer; text: to_Customer; kind:18; commit:/; sort:N", + "label: to_Travel; text: to_Travel; kind:18; commit:/; sort:N", + "label: to_Flight; text: to_Flight; kind:18; commit:/; sort:N", + "label: DraftAdministrativeData; text: DraftAdministrativeData; kind:18; commit:; sort:N", + ]); + }); + it("navigation segment completion (case 2)", async function () { + const result = await getCompletionResult( + `` + ); + expect( + result.map((item) => completionItemToSnapshot(item)) + ).toStrictEqual([ + "label: CurrencyCode; text: CurrencyCode; kind:18; commit:/; sort:N", + "label: BookingStatus; text: BookingStatus; kind:18; commit:/; sort:N", + "label: to_BookSupplement; text: to_BookSupplement; kind:18; commit:/; sort:N", + "label: to_Carrier; text: to_Carrier; kind:18; commit:/; sort:N", + "label: to_Customer; text: to_Customer; kind:18; commit:/; sort:N", + "label: to_Travel; text: to_Travel; kind:18; commit:/; sort:N", + "label: to_Flight; text: to_Flight; kind:18; commit:/; sort:N", + "label: DraftAdministrativeData; text: DraftAdministrativeData; kind:18; commit:; sort:N", + ]); + }); + + it("navigation segment completion leading to entity type the same as entity set type", async function () { + const result = await getCompletionResult( + `` + ); + expect( + result.map((item) => completionItemToSnapshot(item)) + ).toStrictEqual([ + "label: CurrencyCode; text: CurrencyCode; kind:18; commit:/; sort:N", + "label: BookingStatus; text: BookingStatus; kind:18; commit:/; sort:N", + "label: to_Carrier; text: to_Carrier; kind:18; commit:/; sort:N", + "label: to_Customer; text: to_Customer; kind:18; commit:/; sort:N", + "label: to_Travel; text: to_Travel; kind:18; commit:/; sort:N", + "label: to_Flight; text: to_Flight; kind:18; commit:/; sort:N", + "label: DraftAdministrativeData; text: DraftAdministrativeData; kind:18; commit:; sort:N", + ]); + }); + + it("navigation segment completion - no cyclic routes", async function () { + const result = await getCompletionResult( + `` + ); + expect( + result.map((item) => completionItemToSnapshot(item)) + ).toStrictEqual([ + "label: CurrencyCode; text: CurrencyCode; kind:18; commit:/; sort:N", + "label: BookingStatus; text: BookingStatus; kind:18; commit:/; sort:N", + "label: to_Carrier; text: to_Carrier; kind:18; commit:/; sort:N", + "label: to_Customer; text: to_Customer; kind:18; commit:/; sort:N", + "label: to_Flight; text: to_Flight; kind:18; commit:/; sort:N", + "label: DraftAdministrativeData; text: DraftAdministrativeData; kind:18; commit:; sort:N", + ]); + }); + + describe("when contextPath spec contains expected terms", () => { + let constraintsStub: jest.SpyInstance; + beforeAll(() => { + constraintsStub = jest + .spyOn(miscUtils, "getPathConstraintsForControl") + .mockReturnValue({ + expectedAnnotations: [ + { + alias: "UI", + fullyQualifiedName: "com.sap.vocabularies.UI.v1.Chart", + name: "Chart", + }, + ], + expectedTypes: ["EntitySet", "EntityType"], + }); + }); + afterAll(() => { + constraintsStub.mockRestore(); + }); + + it("first segment completion", async function () { + const result = await getCompletionResult( + `` + ); + expect( + result.map((item) => completionItemToSnapshot(item)) + ).toStrictEqual([ + "label: /TravelService.EntityContainer; text: /TravelService.EntityContainer; kind:19; commit:/; sort:Z", + "label: /TravelService.EntityContainer/Travel; text: /TravelService.EntityContainer/Travel; kind:2; commit:/; sort:B", + "label: /TravelService.EntityContainer/Booking; text: /TravelService.EntityContainer/Booking; kind:2; commit:/; sort:B", + "label: /TravelService.EntityContainer/BookedFlights; text: /TravelService.EntityContainer/BookedFlights; kind:2; commit:/; sort:B", + "label: /TravelService.EntityContainer/BookingSupplement; text: /TravelService.EntityContainer/BookingSupplement; kind:2; commit:/; sort:B", + "label: /Travel; text: /Travel; kind:7; commit:; sort:A", + "label: /Booking; text: /Booking; kind:7; commit:/; sort:A", + "label: /BookedFlights; text: /BookedFlights; kind:7; commit:/; sort:A", + "label: /BookingSupplement; text: /BookingSupplement; kind:7; commit:/; sort:A", + ]); + }); + + it("first segment completion after slash", async function () { + const result = await getCompletionResult( + `` + ); + expect(result.map((item) => item.insertText)).toStrictEqual([ + "TravelService.EntityContainer", + "TravelService.EntityContainer/Travel", + "TravelService.EntityContainer/Booking", + "TravelService.EntityContainer/BookedFlights", + "TravelService.EntityContainer/BookingSupplement", + "Travel", + "Booking", + "BookedFlights", + "BookingSupplement", + ]); + }); + + it("entity set completion after container", async function () { + const result = await getCompletionResult( + `` + ); + expect( + result.map((item) => completionItemToSnapshot(item)) + ).toStrictEqual([ + "label: Travel; text: Travel; kind:2; commit:; sort:B", + "label: Booking; text: Booking; kind:2; commit:/; sort:B", + "label: BookedFlights; text: BookedFlights; kind:2; commit:/; sort:B", + "label: BookingSupplement; text: BookingSupplement; kind:2; commit:/; sort:B", + ]); + }); + + it("navigation segment completion", async function () { + const result = await getCompletionResult( + `` + ); + expect( + result.map((item) => completionItemToSnapshot(item)) + ).toStrictEqual([ + "label: to_BookSupplement; text: to_BookSupplement; kind:18; commit:/; sort:N", + "label: to_Travel; text: to_Travel; kind:18; commit:; sort:N", + ]); + }); + + it("navigation segment completion (case 2)", async function () { + const result = await getCompletionResult( + `` + ); + expect( + result.map((item) => completionItemToSnapshot(item)) + ).toStrictEqual([ + "label: to_BookSupplement; text: to_BookSupplement; kind:18; commit:/; sort:N", + "label: to_Travel; text: to_Travel; kind:18; commit:; sort:N", + ]); + }); + + it("navigation segment completion leading to entity type the same as entity set type", async function () { + const result = await getCompletionResult( + `` + ); + expect( + result.map((item) => completionItemToSnapshot(item)) + ).toStrictEqual([ + "label: to_Travel; text: to_Travel; kind:18; commit:; sort:N", + ]); + }); + + it("navigation segment completion - no cyclic routes", async function () { + const result = await getCompletionResult( + `` + ); + expect( + result.map((item) => completionItemToSnapshot(item)) + ).toStrictEqual([]); + }); + }); + + describe("no completion when...", () => { + it("UI5Property not found", async function () { + const result = await getCompletionResult( + ``, + (c) => { + const newClasses = { ...c.ui5Model.classes }; + delete newClasses["sap.fe.macros.Chart"]; + const newContext: Context = { + ...c, + ui5Model: { + ...c.ui5Model, + classes: newClasses, + }, + }; + return newContext; + } + ); + expect(result.length).toEqual(0); + }); + + it("services unavailable", async function () { + const result = await getCompletionResult( + ``, + (c) => ({ ...c, services: {} }) + ); + expect(result.length).toEqual(0); + }); + + it("service path is not provided in manifest", async function () { + const result = await getCompletionResult( + ``, + + (c) => ({ + ...c, + manifestDetails: { + ...c.manifestDetails, + mainServicePath: "", + }, + }) + ); + expect(result.length).toEqual(0); + }); + + it("path is relative", async function () { + const result = await getCompletionResult( + `` + ); + expect(result.length).toEqual(0); + }); + + it("empty metadata", async function () { + const constraintsStub = jest + .spyOn(miscUtils, "getPathConstraintsForControl") + .mockReturnValue({ expectedAnnotations: [], expectedTypes: [] }); + let result: CompletionItem[]; + try { + result = await getCompletionResult( + `` + ); + } finally { + constraintsStub.mockRestore(); + } + expect(result.length).toEqual(0); + }); + + it("existing path target is not resolved", async function () { + const result = await getCompletionResult( + `` + ); + expect(result.length).toEqual(0); + }); + + it("existing path target is a property", async function () { + const result = await getCompletionResult( + `` + ); + expect(result.length).toEqual(0); + }); + }); + }); +}); From f8c31a0a97a0ae6df20e5f0028c93aabbf45f278 Mon Sep 17 00:00:00 2001 From: "GLOBAL\\C5293748" Date: Fri, 17 Nov 2023 21:07:25 +0200 Subject: [PATCH 4/4] fix: cleanup --- packages/context/src/ui5-model.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/context/src/ui5-model.ts b/packages/context/src/ui5-model.ts index cda59f2b1..be4d3391a 100644 --- a/packages/context/src/ui5-model.ts +++ b/packages/context/src/ui5-model.ts @@ -373,7 +373,6 @@ export async function negotiateVersionWithFetcher( if (version === UI5_VERSION_S4_PLACEHOLDER) { useLatestVersion = true; adjustedVersion = version; - // TODO: insert actual number into the message getLogger().warn( `The version specified as minUI5Version in your manifest.json is not supported by Language Assistant, the latest available version is used instead` ); @@ -387,7 +386,7 @@ export async function negotiateVersionWithFetcher( } if (!version) { - // no version defined, using default or latest version + // no version defined, using default version getLogger().warn( "No version defined! Please check the minUI5Version in your manifest.json!" );