diff --git a/extension/src/client/command/RunAction.ts b/extension/src/client/command/RunAction.ts index 1ce2bcc7..0973fc6d 100644 --- a/extension/src/client/command/RunAction.ts +++ b/extension/src/client/command/RunAction.ts @@ -307,7 +307,7 @@ class ActionRunner { if (!fs.existsSync(storagePath)) { fs.mkdirSync(storagePath) } - + // exec await this.mavenProxy.copyDependency( "com.vmware.pscoe.o11n", "exec", diff --git a/extension/src/client/command/TriggerCollection.ts b/extension/src/client/command/TriggerCollection.ts index aff4053e..23c8352a 100644 --- a/extension/src/client/command/TriggerCollection.ts +++ b/extension/src/client/command/TriggerCollection.ts @@ -6,6 +6,8 @@ import { AutoWire, Logger, sleep } from "@vmware/vrdt-common" import { remote } from "@vmware/vro-language-server" import * as vscode from "vscode" +import { LanguageClient } from "vscode-languageclient" +import { CollectionStatus } from "packages/node/vro-language-server/src/server/request/collection/ServerCollection" import { Commands, LanguageServerConfig } from "../constants" import { LanguageServices } from "../lang" @@ -40,33 +42,36 @@ export class TriggerCollection extends Command { location: vscode.ProgressLocation.Window, title: "vRO hint collection" }, - progress => { + async progress => { return new Promise(async (resolve, reject) => { - await languageClient.sendRequest(remote.server.triggerVroCollection, false) - let status = await languageClient.sendRequest(remote.server.giveVroCollectionStatus) - - while (status && !status.finished) { - this.logger.info("Collection status:", status) - progress.report(status) - await sleep(LanguageServerConfig.SleepTime) - status = await languageClient.sendRequest(remote.server.giveVroCollectionStatus) - } - - this.logger.info("Collection finished:", status) + const status: CollectionStatus = await this.triggerVroDataCollection(languageClient, progress) if (status.error !== undefined) { - await vscode.commands.executeCommand(Commands.EventCollectionError, status.error) - if (status.data.hintsPluginBuild === 0) { - vscode.window.showErrorMessage( - "The vRO Hint plug-in is not installed on the configured vRO server" - ) - } - } else { - await vscode.commands.executeCommand(Commands.EventCollectionSuccess) + reject(new Error(`Failed to trigger data collection from vRO: ${status.error}`)) } - resolve() }) } ) } + + private async triggerVroDataCollection(languageClient: LanguageClient, progress: any): Promise { + await languageClient.sendRequest(remote.server.triggerVroCollection, false) + let status = await languageClient.sendRequest(remote.server.giveVroCollectionStatus) + + // wait for status change + while (status && !status.finished) { + this.logger.info("Collection status:", status) + progress.report(status) + await sleep(LanguageServerConfig.SleepTime) + status = await languageClient.sendRequest(remote.server.giveVroCollectionStatus) + } + // check for error response + if (status.error !== undefined) { + await vscode.commands.executeCommand(Commands.EventCollectionError, status.error) + return status + } + await vscode.commands.executeCommand(Commands.EventCollectionSuccess) + + return status + } } diff --git a/extension/src/client/ui/StatusBarController.ts b/extension/src/client/ui/StatusBarController.ts index 47c77342..645ebfd6 100644 --- a/extension/src/client/ui/StatusBarController.ts +++ b/extension/src/client/ui/StatusBarController.ts @@ -53,14 +53,6 @@ export class StatusBarController implements Registrable, vscode.Disposable { this.collectionStatus.dispose() } - private onConfigurationOrProfilesChanged() { - const currentProfileName = this.config.hasActiveProfile() ? this.config.activeProfile.get("id") : undefined - - if (this.verifyConfiguration() && currentProfileName !== this.profileName && this.env.hasRelevantProject()) { - vscode.commands.executeCommand(Commands.TriggerServerCollection) - } - } - verifyConfiguration(): boolean { this.profileName = this.config.hasActiveProfile() ? this.config.activeProfile.get("id") : undefined this.logger.info(`Verifying configuration for active profile ${this.profileName}`) @@ -96,6 +88,14 @@ export class StatusBarController implements Registrable, vscode.Disposable { return false } + private onConfigurationOrProfilesChanged() { + const currentProfileName = this.config.hasActiveProfile() ? this.config.activeProfile.get("id") : undefined + + if (this.verifyConfiguration() && currentProfileName !== this.profileName && this.env.hasRelevantProject()) { + vscode.commands.executeCommand(Commands.TriggerServerCollection) + } + } + private onCollectionStart() { this.collectionButton.text = "$(watch) " this.collectionButton.command = Commands.TriggerServerCollection diff --git a/packages/node/vrdt-common/src/rest/VroRestClient.ts b/packages/node/vrdt-common/src/rest/VroRestClient.ts index 3f3b31bb..10f4155b 100644 --- a/packages/node/vrdt-common/src/rest/VroRestClient.ts +++ b/packages/node/vrdt-common/src/rest/VroRestClient.ts @@ -18,6 +18,7 @@ import { ContentChildrenResponse, ContentLinksResponse, InventoryElement, + LinkItem, LogMessage, Resource, Version, @@ -86,7 +87,7 @@ export class VroRestClient { this.logger.info("Initial authentication...") let auth: Auth - await sleep(1000) // to properly initialize the components + await sleep(3000) // to properly initialize the components let refreshToken = this.refreshToken switch (this.authMethod.toLowerCase()) { @@ -535,18 +536,19 @@ export class VroRestClient { `categories?isRoot=true&categoryType=${categoryType}` ) const categories = responseJson.link - .map(child => { - const name = child.attributes.find(att => att.name === "name") - const id = child.attributes.find(att => att.name === "id") + .map((item: LinkItem) => { + const name = item.attributes.find(att => att.name === "name") + const id = item.attributes.find(att => att.name === "id") return { - name: name ? name.value : undefined, - id: id ? id.value : undefined, + name: name?.value ?? undefined, + id: id?.value ?? undefined, type: categoryType, - rel: child.rel + rel: item?.rel, + description: item?.description?.value ?? undefined } }) - .filter(val => { - return val.name !== undefined && val.id !== undefined + .filter(item => { + return item.name !== undefined && item.id !== undefined }) as ApiElement[] categories.sort((x, y) => x.name.localeCompare(y.name)) diff --git a/packages/node/vrdt-common/src/rest/vro-model.ts b/packages/node/vrdt-common/src/rest/vro-model.ts index d0e81b21..ab6d810e 100644 --- a/packages/node/vrdt-common/src/rest/vro-model.ts +++ b/packages/node/vrdt-common/src/rest/vro-model.ts @@ -79,12 +79,25 @@ export interface InventoryElement extends Resource { href: string } +export interface BaseAttribute { + name: string + value?: string +} + +export interface TypeAttribute extends BaseAttribute { + type: string +} + +export interface LinkItem { + attributes: TypeAttribute[] + rel: string + href: string + description?: BaseAttribute +} + export interface ContentLinksResponse { - link: { - attributes: { name: string; value: string; type: string }[] - rel: string - href: string - }[] + link: LinkItem[] + total?: number } export interface ContentChildrenResponse { diff --git a/packages/node/vro-language-server/src/constants.ts b/packages/node/vro-language-server/src/constants.ts index c9b63cef..cb98fdea 100644 --- a/packages/node/vro-language-server/src/constants.ts +++ b/packages/node/vro-language-server/src/constants.ts @@ -40,7 +40,9 @@ message Action { }` export const Timeout = { - ONE_SECOND: 1000 + ONE_SECOND: 1000, + THREE_SECONDS: 3000, + FIVE_SECONDS: 5000 } export enum CompletionPrefixKind { diff --git a/packages/node/vro-language-server/src/server/core/HintLookup.ts b/packages/node/vro-language-server/src/server/core/HintLookup.ts index 03865524..5a6e4722 100644 --- a/packages/node/vro-language-server/src/server/core/HintLookup.ts +++ b/packages/node/vro-language-server/src/server/core/HintLookup.ts @@ -37,14 +37,12 @@ class HintStore { @AutoWire export class HintLookup implements Disposable { private readonly logger = Logger.get("HintLookup") - private scriptingApi: HintStore = new HintStore() - actions: any = new HintStore() private configs: HintStore = new HintStore() private vroModulesAndActions: HintModule[] private vroObjects: vmw.pscoe.hints.IClass[] - private subscriptions: Disposable[] = [] + actions: any = new HintStore() constructor( private environment: Environment, @@ -61,7 +59,7 @@ export class HintLookup implements Disposable { dispose(): void { this.logger.debug("Disposing HintLookup") - this.subscriptions.forEach(s => s && s.dispose()) + this.subscriptions.forEach(s => s?.dispose()) } getGlobalActionsPack() { @@ -93,6 +91,7 @@ export class HintLookup implements Disposable { ? _.flatMap(this.actions.local[workspaceFolder.uri.fsPath], pack => pack.modules) : [] const globalModules = _.flatMap(this.actions.global, pack => pack.modules) + return _.unionWith(localModules, globalModules, (x, y) => x.name === y.name) } @@ -100,7 +99,7 @@ export class HintLookup implements Disposable { const module = this.getActionModules(workspaceFolder).find(module => module.name === moduleName) this.logger.debug(`Module hint: ${JSON.stringify(module, null, 4)}`) - if (module && module.actions) { + if (module?.actions) { return module.actions.filter(action => !!action) } @@ -116,13 +115,14 @@ export class HintLookup implements Disposable { ? _.flatMap(this.configs.local[workspaceFolder.uri.fsPath], pack => pack.categories) : [] const globalCategories = _.flatMap(this.configs.global, pack => pack.categories) + return _.unionWith(localCategories, globalCategories, (x, y) => x.path === y.path) } getConfigElementsIn(categoryPath: string, workspaceFolder?: WorkspaceFolder): vmw.pscoe.hints.IConfig[] { const module = this.getConfigCategories(workspaceFolder).find(category => category.path === categoryPath) - if (module && module.configurations) { + if (module?.configurations) { return module.configurations.filter(config => !!config) } @@ -135,13 +135,10 @@ export class HintLookup implements Disposable { if (this.vroObjects) { result.push(...this.vroObjects) } - for (const api of this.scriptingApi.global) { for (const cls of api.classes) { const hasConstructors = !!cls.constructors && cls.constructors.length > 0 - if (filter.isInstantiable === undefined) { - result.push(cls) - } else if (hasConstructors === filter.isInstantiable) { + if (filter.isInstantiable === undefined || hasConstructors === filter.isInstantiable) { result.push(cls) } } @@ -165,12 +162,15 @@ export class HintLookup implements Disposable { // // Event Handlers // - initialize() { this.environment.workspaceFolders.forEach(this.load, this) this.load() } + refreshForWorkspace(workspaceFolder: WorkspaceFolder): void { + this.load(workspaceFolder) + } + private onDidChangeConfiguration(): void { this.logger.debug("HintLookup.onDidChangeConfiguration()") this.load() @@ -183,14 +183,9 @@ export class HintLookup implements Disposable { } } - refreshForWorkspace(workspaceFolder: WorkspaceFolder): void { - this.load(workspaceFolder) - } - // // Load proto files // - private load(workspaceFolder?: WorkspaceFolder): void { const actionsFile = this.environment.resolveHintFile("actions.pb", workspaceFolder) if (actionsFile) { @@ -208,7 +203,6 @@ export class HintLookup implements Disposable { ) } } - const configsFile = this.environment.resolveHintFile("configs.pb", workspaceFolder) if (configsFile) { this.loadProtoInScope([configsFile], workspaceFolder, this.configs, vmw.pscoe.hints.ConfigurationsPack) @@ -218,7 +212,6 @@ export class HintLookup implements Disposable { // plugins aren't located in workspace folder const coreApiFile = this.environment.resolveHintFile("core-api.pb", undefined) const pluginFiles = this.environment.resolvePluginHintFiles() - if (coreApiFile) { pluginFiles.push(coreApiFile) } @@ -244,7 +237,6 @@ export class HintLookup implements Disposable { result.push(hintPack) } }) - if (!scope) { target.global = result } else { @@ -259,8 +251,8 @@ export class HintLookup implements Disposable { this.logger.warn(`Hint file '${filePath}' does not exist`) return null } - const buffer = fs.readFileSync(filePath) + return decoder.decode(buffer) } } diff --git a/packages/node/vro-language-server/src/server/feature/CompletionProvider.ts b/packages/node/vro-language-server/src/server/feature/CompletionProvider.ts index f6e9e40c..6da5d3eb 100644 --- a/packages/node/vro-language-server/src/server/feature/CompletionProvider.ts +++ b/packages/node/vro-language-server/src/server/feature/CompletionProvider.ts @@ -177,6 +177,38 @@ export class CompletionProvider { return suggestions } + private getConstructorSuggestions(prefix: CompletionPrefix): CompletionItem[] { + const suggestions: CompletionItem[] = [] + + this.hints + .getClasses({ isInstantiable: true }) + .filter(cls => !!cls.name && cls.name.startsWith(prefix.filter || "")) + .forEach(cls => { + if (cls.constructors && cls.constructors.length > 0) { + for (const constr of cls.constructors) { + const name = cls.name ?? "" + const completionItem = CompletionItem.create(name) + completionItem.kind = CompletionItemKind.Constructor + + if (constr.description) { + completionItem.documentation = Previewer.extendDescriptionWithParams( + constr.description, + constr.parameters + ) + } else if (cls.description) { + completionItem.documentation = cls.description + } + + completionItem.detail = Previewer.computeDetailsForConstructor(cls, constr) + completionItem.sortText = `000${name}` + suggestions.push(completionItem) + } + } + }) + + return suggestions + } + private getStaticMemberSuggestions(prefix: CompletionPrefix): CompletionItem[] { // TODO: Set isInstantiable back to `false`, once there are other ways to find out // the members of non-static classes. At the moment, the only way to do that @@ -185,15 +217,19 @@ export class CompletionProvider { .getClasses({ isInstantiable: undefined }) .concat(this.hints.getFunctionSets()) .find(c => c.name === prefix.value) - - if (!cls) return [] + if (!cls) { + return [] + } const suggestions: CompletionItem[] = [] const methods: CompletionItem[] = this.getMethodsSuggestions(cls, prefix) const properties: CompletionItem[] = this.getPropertiesSuggestions(cls, prefix) - - methods.forEach(item => suggestions.push(item)) - properties.forEach(item => suggestions.push(item)) + if (methods?.length) { + methods.forEach(item => suggestions.push(item)) + } + if (properties?.length) { + properties.forEach(item => suggestions.push(item)) + } return suggestions } @@ -249,38 +285,6 @@ export class CompletionProvider { return suggestions } - private getConstructorSuggestions(prefix: CompletionPrefix): CompletionItem[] { - const suggestions: CompletionItem[] = [] - - this.hints - .getClasses({ isInstantiable: true }) - .filter(cls => !!cls.name && cls.name.startsWith(prefix.filter || "")) - .forEach(cls => { - if (cls.constructors && cls.constructors.length > 0) { - for (const constr of cls.constructors) { - const name = cls.name ?? "" - const completionItem = CompletionItem.create(name) - completionItem.kind = CompletionItemKind.Constructor - - if (constr.description) { - completionItem.documentation = Previewer.extendDescriptionWithParams( - constr.description, - constr.parameters - ) - } else if (cls.description) { - completionItem.documentation = cls.description - } - - completionItem.detail = Previewer.computeDetailsForConstructor(cls, constr) - completionItem.sortText = `000${name}` - suggestions.push(completionItem) - } - } - }) - - return suggestions - } - private getModuleClassSuggestions(prefix: CompletionPrefix, workspaceFolder: WorkspaceFolder): CompletionItem[] { const suggestions = this.hints .getActionsIn(prefix.value, workspaceFolder) @@ -306,7 +310,6 @@ export class CompletionProvider { const lineContent = document.getLineContentUntil(position) this.logger.debug(`Trying to provide auto completion for line '${lineContent}'`) - for (const pattern of prefixPatterns) { const prefix = pattern.match(lineContent) if (prefix) { @@ -314,8 +317,8 @@ export class CompletionProvider { return prefix } } - this.logger.debug("None of the patterns matched.") + return null } } diff --git a/packages/node/vro-language-server/src/server/request/collection/ServerCollection.ts b/packages/node/vro-language-server/src/server/request/collection/ServerCollection.ts index 84a25c41..21304fc5 100644 --- a/packages/node/vro-language-server/src/server/request/collection/ServerCollection.ts +++ b/packages/node/vro-language-server/src/server/request/collection/ServerCollection.ts @@ -4,12 +4,13 @@ */ import { CancellationToken } from "vscode-languageserver" -import { AutoWire, HintAction, HintModule, HintPlugin, Logger, VroRestClient } from "@vmware/vrdt-common" +import { AutoWire, HintAction, HintModule, HintPlugin, Logger, sleep, VroRestClient } from "@vmware/vrdt-common" import { remote } from "../../../public" import { ConnectionLocator, Environment, HintLookup, Settings } from "../../core" import { WorkspaceCollection } from "./WorkspaceCollection" import { vmw } from "../../../proto" +import { Timeout } from "../../../constants" @AutoWire export class CollectionStatus { @@ -97,6 +98,7 @@ export class ServerCollection { async getModulesAndActions() { this.logger.info("Collecting Modules and Actions...") + // fetch the root script module categories const modules: HintModule[] = (await this.restClient.getRootCategories("ScriptModuleCategory")).map(module => { return { id: module.id, @@ -104,24 +106,18 @@ export class ServerCollection { actions: [] } }) - await Promise.all( - modules.map( - async module => - (module.actions = await this.restClient.getChildrenOfCategoryWithDetails(module.id).then(actions => - actions.map(action => { - return { - id: action.id, - name: action.name, - version: action.version, - description: action.description, - returnType: action.returnType, - parameters: action.parameters - } as HintAction - }) - )) - ) - ) + // add delay between the 2 REST calls in order not to overload the vRO vco service cache + // see also: https://kb.vmware.com/s/article/95783?lang=en_US + await this.setDelay(Timeout.THREE_SECONDS) + + // Enrichment of category actions execution has to be executed in serial order for not to overload the vRO + // see also: https://kb.vmware.com/s/article/95783?lang=en_US + for (const module of modules) { + await this.enrichHintModuleWithActions(module) + } + this.logger.info("Modules and Actions collected from vRO") + return modules } @@ -166,10 +162,8 @@ export class ServerCollection { if (!link) { throw new Error(`No plugin details found`) } - const parsedLink = link[0].substring(9).toString() // always retrieve and parse the first occurrence const pluginDetails = await this.restClient.getPluginDetails(parsedLink) - for (const pluginObject of pluginDetails["objects"]) { const object: vmw.pscoe.hints.IClass = { name: pluginObject["name"] @@ -196,4 +190,15 @@ export class ServerCollection { this.currentStatus.error = message this.currentStatus.finished = true } + + private async setDelay(delayMs: number) { + await sleep(delayMs) + } + + private async enrichHintModuleWithActions(module: HintModule): Promise { + const actions: HintAction[] = await this.restClient.getChildrenOfCategoryWithDetails(module.id) + module.actions = actions + + return module + } } diff --git a/packages/node/vro-language-server/src/server/request/collection/WorkspaceCollection.ts b/packages/node/vro-language-server/src/server/request/collection/WorkspaceCollection.ts index 87db92d7..64edabd6 100644 --- a/packages/node/vro-language-server/src/server/request/collection/WorkspaceCollection.ts +++ b/packages/node/vro-language-server/src/server/request/collection/WorkspaceCollection.ts @@ -68,6 +68,7 @@ export class WorkspaceCollection { async triggerCollectionAndRefresh(workspaceFolder: WorkspaceFolder): Promise { await this.triggerCollection(workspaceFolder) + // workspace collection this.hints.refreshForWorkspace(workspaceFolder) } @@ -84,7 +85,6 @@ export class WorkspaceCollection { const fullPath = path.join(workspaceFolder.uri.fsPath, modules.join(",")) const modulesPath = path.join(fullPath, "src/main/resources") - try { const payload = this.collectLocalData(modulesPath) this.generateActionsPbFiles(payload, outputDir, workspaceFolder)