From ee9e898f979ac921485cb5cfbbc84a514a4fd077 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sat, 12 Mar 2022 15:12:22 +0100 Subject: [PATCH 1/5] Add clean workflow command This command edits the current workflow document to remove the non-workflow logic related properties --- client/src/commands/cleanWorkflow.ts | 28 +++++++ client/src/commands/setup.ts | 2 + client/src/requestsDefinitions.ts | 15 ++++ package.json | 11 +++ server/src/commands/cleanWorkflow.ts | 95 ++++++++++++++++++---- server/src/commands/requestsDefinitions.ts | 15 ++++ 6 files changed, 152 insertions(+), 14 deletions(-) create mode 100644 client/src/commands/cleanWorkflow.ts diff --git a/client/src/commands/cleanWorkflow.ts b/client/src/commands/cleanWorkflow.ts new file mode 100644 index 0000000..0e05b71 --- /dev/null +++ b/client/src/commands/cleanWorkflow.ts @@ -0,0 +1,28 @@ +import { window } from "vscode"; +import { CleanWorkflowDocumentParams, CleanWorkflowDocumentRequest } from "../requestsDefinitions"; +import { CustomCommand, getCommandFullIdentifier } from "./common"; + +/** + * Command to 'clean' the selected workflow document. + * It will apply edits to remove the non-workflow logic related parts. + */ +export class CleanWorkflowCommand extends CustomCommand { + public static id = getCommandFullIdentifier("cleanWorkflow"); + readonly identifier: string = CleanWorkflowCommand.id; + + async execute(args: any[]): Promise { + if (!window.activeTextEditor) { + return; + } + const { document } = window.activeTextEditor; + + const params: CleanWorkflowDocumentParams = { uri: this.client.code2ProtocolConverter.asUri(document.uri) }; + const result = await this.client.sendRequest(CleanWorkflowDocumentRequest.type, params); + if (!result) { + throw new Error("Cannot clean the requested document. The server returned no result."); + } + if (result.error) { + throw new Error(result.error); + } + } +} diff --git a/client/src/commands/setup.ts b/client/src/commands/setup.ts index c5d0715..2cfdf28 100644 --- a/client/src/commands/setup.ts +++ b/client/src/commands/setup.ts @@ -5,6 +5,7 @@ import { SelectForCleanCompareCommand } from "./selectForCleanCompare"; import { PreviewCleanWorkflowCommand } from "./previewCleanWorkflow"; import { CleanWorkflowProvider } from "../providers/cleanWorkflowProvider"; import { GitProvider } from "../providers/git/common"; +import { CleanWorkflowCommand } from "./cleanWorkflow"; /** * Registers all custom commands declared in package.json @@ -13,6 +14,7 @@ import { GitProvider } from "../providers/git/common"; */ export function setupCommands(context: ExtensionContext, client: CommonLanguageClient, gitProvider: GitProvider) { context.subscriptions.push(new PreviewCleanWorkflowCommand(client).register()); + context.subscriptions.push(new CleanWorkflowCommand(client).register()); const selectForCompareProvider = new SelectForCleanCompareCommand(client); context.subscriptions.push(selectForCompareProvider.register()); context.subscriptions.push( diff --git a/client/src/requestsDefinitions.ts b/client/src/requestsDefinitions.ts index 4fa45bc..9771537 100644 --- a/client/src/requestsDefinitions.ts +++ b/client/src/requestsDefinitions.ts @@ -3,9 +3,18 @@ import { RequestType } from "vscode-languageclient"; // TODO: Move the contents of this file to a shared lib https://github.com/Microsoft/vscode/issues/15829 export namespace LSRequestIdentifiers { + export const CLEAN_WORKFLOW_DOCUMENT = "galaxy-workflows-ls.cleanWorkflowDocument"; export const CLEAN_WORKFLOW_CONTENTS = "galaxy-workflows-ls.cleanWorkflowContents"; } +export interface CleanWorkflowDocumentParams { + uri: string; +} + +export interface CleanWorkflowDocumentResult { + error: string; +} + export interface CleanWorkflowContentsParams { contents: string; } @@ -14,6 +23,12 @@ export interface CleanWorkflowContentsResult { contents: string; } +export namespace CleanWorkflowDocumentRequest { + export const type = new RequestType( + LSRequestIdentifiers.CLEAN_WORKFLOW_DOCUMENT + ); +} + export namespace CleanWorkflowContentsRequest { export const type = new RequestType( LSRequestIdentifiers.CLEAN_WORKFLOW_CONTENTS diff --git a/package.json b/package.json index cfa00a2..68528a6 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,13 @@ "icon": "$(eye)", "category": "Galaxy Workflows" }, + { + "command": "galaxy-workflows.cleanWorkflow", + "title": "Clean workflow", + "enablement": "resourceLangId == galaxyworkflow", + "icon": "$(edit)", + "category": "Galaxy Workflows" + }, { "command": "galaxy-workflows.selectForCleanCompare", "title": "Select workflow for (clean) compare", @@ -71,6 +78,10 @@ "command": "galaxy-workflows.previewCleanWorkflow", "when": "activeEditor" }, + { + "command": "galaxy-workflows.cleanWorkflow", + "when": "activeEditor" + }, { "command": "galaxy-workflows.selectForCleanCompare", "when": "false" diff --git a/server/src/commands/cleanWorkflow.ts b/server/src/commands/cleanWorkflow.ts index 4935a38..3faf693 100644 --- a/server/src/commands/cleanWorkflow.ts +++ b/server/src/commands/cleanWorkflow.ts @@ -1,8 +1,16 @@ +import { ApplyWorkspaceEditParams, Range, TextDocumentEdit, TextEdit } from "vscode-languageserver"; import { TextDocument } from "vscode-languageserver-textdocument"; import { ASTNode, PropertyASTNode, WorkflowDocument } from "../languageTypes"; import { GalaxyWorkflowLanguageServer } from "../server"; import { CustomCommand } from "./common"; -import { CleanWorkflowContentsRequest, CleanWorkflowContentsResult } from "./requestsDefinitions"; +import { + CleanWorkflowContentsParams, + CleanWorkflowContentsRequest, + CleanWorkflowContentsResult, + CleanWorkflowDocumentParams, + CleanWorkflowDocumentRequest, + CleanWorkflowDocumentResult, +} from "./requestsDefinitions"; const CLEANABLE_PROPERTY_NAMES = new Set(["position", "uuid", "errors", "version"]); @@ -16,21 +24,69 @@ export class CleanWorkflowCommand extends CustomCommand { } protected listenToRequests(): void { - this.connection.onRequest(CleanWorkflowContentsRequest.type, async (params) => { - const tempDocument = this.createTempWorkflowDocumentWithContents(params.contents); - const workflowDocument = this.languageService.parseWorkflowDocument(tempDocument); + this.connection.onRequest(CleanWorkflowContentsRequest.type, (params) => + this.onCleanWorkflowContentsRequest(params) + ); + this.connection.onRequest(CleanWorkflowDocumentRequest.type, (params) => + this.onCleanWorkflowDocumentRequest(params) + ); + } + + private async onCleanWorkflowContentsRequest( + params: CleanWorkflowContentsParams + ): Promise { + const tempDocument = this.createTempWorkflowDocumentWithContents(params.contents); + const workflowDocument = this.languageService.parseWorkflowDocument(tempDocument); + if (workflowDocument) { + return await this.cleanWorkflowContentsResult(workflowDocument); + } + return undefined; + } + + private async onCleanWorkflowDocumentRequest( + params: CleanWorkflowDocumentParams + ): Promise { + try { + const workflowDocument = this.workflowDocuments.get(params.uri); if (workflowDocument) { - return await this.CleanWorkflowContentsResult(workflowDocument); + const edits = this.getTextEditsToCleanWorkflow(workflowDocument); + const editParams: ApplyWorkspaceEditParams = { + label: "Clean workflow", + edit: { + documentChanges: [ + TextDocumentEdit.create( + { + uri: params.uri, + version: null, + }, + edits + ), + ], + }, + }; + this.connection.workspace.applyEdit(editParams); } - return undefined; + return { error: "" }; + } catch (error) { + return { error: String(error) }; + } + } + + private getTextEditsToCleanWorkflow(workflowDocument: WorkflowDocument): TextEdit[] { + const nodesToRemove = this.getNonEssentialNodes(workflowDocument.jsonDocument.root); + const changes: TextEdit[] = []; + nodesToRemove.forEach((node) => { + const range = this.getReplaceRange(workflowDocument.textDocument, node); + changes.push(TextEdit.replace(range, "")); }); + return changes; } private createTempWorkflowDocumentWithContents(contents: string) { return TextDocument.create("temp://temp-workflow", "galaxyworkflow", 0, contents); } - private async CleanWorkflowContentsResult(workflowDocument: WorkflowDocument): Promise { + private async cleanWorkflowContentsResult(workflowDocument: WorkflowDocument): Promise { const nodesToRemove = this.getNonEssentialNodes(workflowDocument.jsonDocument.root); const contents = this.getCleanContents(workflowDocument.textDocument.getText(), nodesToRemove.reverse()); const result: CleanWorkflowContentsResult = { @@ -79,17 +135,28 @@ export class CleanWorkflowCommand extends CustomCommand { const removeChunks: string[] = []; let result = documentText; nodesToRemove.forEach((node) => { - let startPos = node.offset; - let endPos = node.offset + node.length; - startPos = documentText.lastIndexOf("\n", startPos); - if (documentText.charAt(endPos) === ",") { - endPos++; - } - removeChunks.push(documentText.substring(startPos, endPos)); + const rangeOffsets = this.getFullNodeRangeOffsets(documentText, node); + removeChunks.push(documentText.substring(rangeOffsets.start, rangeOffsets.end)); }); removeChunks.forEach((chunk) => { result = result.replace(chunk, ""); }); return result; } + + private getFullNodeRangeOffsets(documentText: string, node: ASTNode) { + let startPos = node.offset; + let endPos = node.offset + node.length; + startPos = documentText.lastIndexOf("\n", startPos); + if (documentText.charAt(endPos) === ",") { + endPos++; + } + return { start: startPos, end: endPos }; + } + + private getReplaceRange(document: TextDocument, node: ASTNode): Range { + const documentText = document.getText(); + const rangeOffsets = this.getFullNodeRangeOffsets(documentText, node); + return Range.create(document.positionAt(rangeOffsets.start), document.positionAt(rangeOffsets.end)); + } } diff --git a/server/src/commands/requestsDefinitions.ts b/server/src/commands/requestsDefinitions.ts index 65364eb..18f1824 100644 --- a/server/src/commands/requestsDefinitions.ts +++ b/server/src/commands/requestsDefinitions.ts @@ -3,9 +3,18 @@ import { RequestType } from "vscode-languageserver"; // TODO: Move the contents of this file to a shared lib https://github.com/Microsoft/vscode/issues/15829 export namespace LSRequestIdentifiers { + export const CLEAN_WORKFLOW_DOCUMENT = "galaxy-workflows-ls.cleanWorkflowDocument"; export const CLEAN_WORKFLOW_CONTENTS = "galaxy-workflows-ls.cleanWorkflowContents"; } +export interface CleanWorkflowDocumentParams { + uri: string; +} + +export interface CleanWorkflowDocumentResult { + error: string; +} + export interface CleanWorkflowContentsParams { contents: string; } @@ -14,6 +23,12 @@ export interface CleanWorkflowContentsResult { contents: string; } +export namespace CleanWorkflowDocumentRequest { + export const type = new RequestType( + LSRequestIdentifiers.CLEAN_WORKFLOW_DOCUMENT + ); +} + export namespace CleanWorkflowContentsRequest { export const type = new RequestType( LSRequestIdentifiers.CLEAN_WORKFLOW_CONTENTS From 981ce8a439e19165ca3d15de70eacbd6d11c53b1 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sat, 12 Mar 2022 15:24:07 +0100 Subject: [PATCH 2/5] Add clean workflow command to editor context menu --- package.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/package.json b/package.json index 68528a6..4600f11 100644 --- a/package.json +++ b/package.json @@ -114,6 +114,12 @@ "group": "3_compare@2", "when": "config.git.enabled && !git.missing && galaxy-workflows.gitProviderInitialized" } + ], + "editor/context": [ + { + "command": "galaxy-workflows.cleanWorkflow", + "when": "resourceLangId == galaxyworkflow" + } ] } }, From fbef85a411d201c3dcf4ebb2d7a0e247a33a051f Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sat, 12 Mar 2022 15:27:13 +0100 Subject: [PATCH 3/5] Disable workflow document registration debug logging --- server/src/models/workflowDocuments.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/models/workflowDocuments.ts b/server/src/models/workflowDocuments.ts index 06e8084..3e4ea7b 100644 --- a/server/src/models/workflowDocuments.ts +++ b/server/src/models/workflowDocuments.ts @@ -16,17 +16,17 @@ export class WorkflowDocuments { } public addOrReplaceWorkflowDocument(document: WorkflowDocument) { - console.log("registered workflow file: ", document.documentUri); this._documentsCache.set(document.documentUri, document); + //console.debug("workflow files registered: ", this._documentsCache.size); } public removeWorkflowDocument(documentUri: string) { - console.log("unregistered workflow file: ", documentUri); this._documentsCache.delete(documentUri); + //console.debug("workflow files registered: ", this._documentsCache.size); } public dispose() { this._documentsCache.clear(); - console.log("workflow document cache cleared"); + //console.debug("workflow document cache cleared"); } } From 1afc2df0a8eec754c4055479eabc89be3cc3ee11 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sat, 12 Mar 2022 15:54:32 +0100 Subject: [PATCH 4/5] Add preview clean workflow command to context menu --- package.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/package.json b/package.json index 4600f11..71a4ab9 100644 --- a/package.json +++ b/package.json @@ -116,8 +116,14 @@ } ], "editor/context": [ + { + "command": "galaxy-workflows.previewCleanWorkflow", + "group": "galaxyworkflow@1", + "when": "resourceLangId == galaxyworkflow" + }, { "command": "galaxy-workflows.cleanWorkflow", + "group": "galaxyworkflow@2", "when": "resourceLangId == galaxyworkflow" } ] From 0353cc553404f60a33c839e74c8d52a13261b867 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Sat, 12 Mar 2022 16:08:32 +0100 Subject: [PATCH 5/5] Add some code documentation --- server/src/commands/cleanWorkflow.ts | 42 +++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/server/src/commands/cleanWorkflow.ts b/server/src/commands/cleanWorkflow.ts index 3faf693..5f5258b 100644 --- a/server/src/commands/cleanWorkflow.ts +++ b/server/src/commands/cleanWorkflow.ts @@ -12,8 +12,19 @@ import { CleanWorkflowDocumentResult, } from "./requestsDefinitions"; +/** + * A set of property names that are unrelated to the workflow logic. + * Usually used by other tools like the workflow editor. + */ const CLEANABLE_PROPERTY_NAMES = new Set(["position", "uuid", "errors", "version"]); +/** + * Command for handling workflow `cleaning` requests. + * Supports both, direct contents (raw document text), and document uri requests + * for cleaning. + * When requesting with a document uri, the workflow document must be already registered in the server + * as a workflow document. + */ export class CleanWorkflowCommand extends CustomCommand { public static register(server: GalaxyWorkflowLanguageServer): CleanWorkflowCommand { return new CleanWorkflowCommand(server); @@ -32,6 +43,12 @@ export class CleanWorkflowCommand extends CustomCommand { ); } + /** + * Processes a `CleanWorkflowContentsRequest` by returning the `clean` contents + * of a workflow document given the raw text contents of the workflow document. + * @param params The request parameters containing the raw text contents of the workflow + * @returns The `clean` contents of the workflow document + */ private async onCleanWorkflowContentsRequest( params: CleanWorkflowContentsParams ): Promise { @@ -43,6 +60,12 @@ export class CleanWorkflowCommand extends CustomCommand { return undefined; } + /** + * Applies the necessary text edits to the workflow document identified by the given URI to + * remove all the properties in the workflow that are unrelated to the essential workflow logic. + * @param params The request parameters containing the URI of the workflow document. + * @returns An error message if something went wrong + */ private async onCleanWorkflowDocumentRequest( params: CleanWorkflowDocumentParams ): Promise { @@ -73,7 +96,7 @@ export class CleanWorkflowCommand extends CustomCommand { } private getTextEditsToCleanWorkflow(workflowDocument: WorkflowDocument): TextEdit[] { - const nodesToRemove = this.getNonEssentialNodes(workflowDocument.jsonDocument.root); + const nodesToRemove = this.getNonEssentialNodes(workflowDocument, CLEANABLE_PROPERTY_NAMES); const changes: TextEdit[] = []; nodesToRemove.forEach((node) => { const range = this.getReplaceRange(workflowDocument.textDocument, node); @@ -87,7 +110,7 @@ export class CleanWorkflowCommand extends CustomCommand { } private async cleanWorkflowContentsResult(workflowDocument: WorkflowDocument): Promise { - const nodesToRemove = this.getNonEssentialNodes(workflowDocument.jsonDocument.root); + const nodesToRemove = this.getNonEssentialNodes(workflowDocument, CLEANABLE_PROPERTY_NAMES); const contents = this.getCleanContents(workflowDocument.textDocument.getText(), nodesToRemove.reverse()); const result: CleanWorkflowContentsResult = { contents: contents, @@ -95,7 +118,11 @@ export class CleanWorkflowCommand extends CustomCommand { return result; } - private getNonEssentialNodes(root: ASTNode | undefined): PropertyASTNode[] { + private getNonEssentialNodes( + workflowDocument: WorkflowDocument, + cleanablePropertyNames: Set + ): PropertyASTNode[] { + const root = workflowDocument.jsonDocument.root; if (!root) { return []; } @@ -113,7 +140,7 @@ export class CleanWorkflowCommand extends CustomCommand { } else if (node.type === "object") { node.properties.forEach((property: PropertyASTNode) => { const key = property.keyNode.value; - if (CLEANABLE_PROPERTY_NAMES.has(key)) { + if (cleanablePropertyNames.has(key)) { result.push(property); } if (property.valueNode) { @@ -144,6 +171,13 @@ export class CleanWorkflowCommand extends CustomCommand { return result; } + /** + * Gets the range offsets (`start` and `end`) for a given syntax node including + * the blank spaces/indentation before and after the node and possible ending comma. + * @param documentText The full workflow document text + * @param node The syntax node + * @returns The `start` and `end` offsets for the given syntax node + */ private getFullNodeRangeOffsets(documentText: string, node: ASTNode) { let startPos = node.offset; let endPos = node.offset + node.length;