From 69c684ed796f4d1af5f334052aab98b6e1095b84 Mon Sep 17 00:00:00 2001 From: Brett Saviano Date: Thu, 22 Feb 2024 07:30:04 -0500 Subject: [PATCH 1/2] Fire source control hooks when creating/opening/editing/deleting projects (#1313) --- src/commands/project.ts | 69 ++++++++++++++++++- src/commands/studio.ts | 55 +++++++++++---- src/commands/unitTest.ts | 9 ++- src/explorer/explorer.ts | 15 ++++ src/explorer/models/projectNode.ts | 9 +++ .../FileSystemProvider/FileSearchProvider.ts | 15 +++- .../FileSystemProvider/FileSystemProvider.ts | 11 ++- .../FileSystemProvider/TextSearchProvider.ts | 13 ++++ 8 files changed, 176 insertions(+), 20 deletions(-) diff --git a/src/commands/project.ts b/src/commands/project.ts index 99a5c9b8..d614b04e 100644 --- a/src/commands/project.ts +++ b/src/commands/project.ts @@ -12,6 +12,7 @@ import { isCSPFile } from "../providers/FileSystemProvider/FileSystemProvider"; import { notNull, outputChannel } from "../utils"; import { pickServerAndNamespace } from "./addServerNamespaceToWorkspace"; import { exportList } from "./export"; +import { OtherStudioAction, StudioActions } from "./studio"; export interface ProjectItem { Name: string; @@ -137,6 +138,21 @@ export async function createProject(node: NodeBase | undefined, api?: AtelierAPI return; } + // Technically a project is a "document", so tell the server that we created it + try { + const studioActions = new StudioActions(); + await studioActions.fireProjectUserAction(api, name, OtherStudioAction.CreatedNewDocument); + await studioActions.fireProjectUserAction(api, name, OtherStudioAction.FirstTimeDocumentSave); + } catch (error) { + let message = `Source control actions failed for project '${name}'.`; + if (error && error.errorText && error.errorText !== "") { + outputChannel.appendLine("\n" + error.errorText); + outputChannel.show(true); + message += " Check 'ObjectScript' output channel for details."; + } + vscode.window.showErrorMessage(message, "Dismiss"); + } + // Refresh the explorer projectsExplorerProvider.refresh(); @@ -179,6 +195,19 @@ export async function deleteProject(node: ProjectNode | undefined): Promise return vscode.window.showErrorMessage(message, "Dismiss"); } + // Technically a project is a "document", so tell the server that we deleted it + try { + await new StudioActions().fireProjectUserAction(api, project, OtherStudioAction.DeletedDocument); + } catch (error) { + let message = `'DeletedDocument' source control action failed for project '${project}'.`; + if (error && error.errorText && error.errorText !== "") { + outputChannel.appendLine("\n" + error.errorText); + outputChannel.show(true); + message += " Check 'ObjectScript' output channel for details."; + } + vscode.window.showErrorMessage(message, "Dismiss"); + } + // Refresh the explorer projectsExplorerProvider.refresh(); @@ -706,6 +735,12 @@ export async function modifyProject( return; } } + + // Technically a project is a "document", so tell the server that we're opening it + await new StudioActions().fireProjectUserAction(api, project, OtherStudioAction.OpenedDocument).catch(() => { + // Swallow error because showing it is more disruptive than using a potentially outdated project definition + }); + let items: ProjectItem[] = await api .actionQuery("SELECT Name, Type FROM %Studio.Project_ProjectItemsList(?,?) WHERE Type != 'GBL'", [project, "1"]) .then((data) => data.result.content); @@ -862,6 +897,23 @@ export async function modifyProject( } try { + if (add.length || remove.length) { + // Technically a project is a "document", so tell the server that we're editing it + const studioActions = new StudioActions(); + await studioActions.fireProjectUserAction(api, project, OtherStudioAction.AttemptedEdit); + if (studioActions.projectEditAnswer != "1") { + // Don't perform the edit + if (studioActions.projectEditAnswer == "-1") { + // Source control action failed + vscode.window.showErrorMessage( + `'AttemptedEdit' source control action failed for project '${project}'. Check the 'ObjectScript' Output channel for details.`, + "Dismiss" + ); + } + return; + } + } + if (remove.length) { // Delete the obsolete items await api.actionQuery( @@ -900,7 +952,7 @@ export async function modifyProject( // Refresh the files explorer if there's an isfs folder for this project if (node == undefined && isfsFolderForProject(project, node ?? api.configName) != -1) { - await vscode.commands.executeCommand("workbench.files.action.refreshFilesExplorer"); + vscode.commands.executeCommand("workbench.files.action.refreshFilesExplorer"); } } } @@ -1070,6 +1122,21 @@ export async function addIsfsFileToProject( try { if (add.length) { + // Technically a project is a "document", so tell the server that we're editing it + const studioActions = new StudioActions(); + await studioActions.fireProjectUserAction(api, project, OtherStudioAction.AttemptedEdit); + if (studioActions.projectEditAnswer != "1") { + // Don't perform the edit + if (studioActions.projectEditAnswer == "-1") { + // Source control action failed + vscode.window.showErrorMessage( + `'AttemptedEdit' source control action failed for project '${project}'. Check the 'ObjectScript' Output channel for details.`, + "Dismiss" + ); + } + return; + } + // Add any new items await api.actionQuery( `INSERT INTO %Studio.ProjectItem (Project,Name,Type) SELECT * FROM (${add diff --git a/src/commands/studio.ts b/src/commands/studio.ts index b20e2822..76b4e8db 100644 --- a/src/commands/studio.ts +++ b/src/commands/studio.ts @@ -6,7 +6,6 @@ import { DocumentContentProvider } from "../providers/DocumentContentProvider"; import { ClassNode } from "../explorer/models/classNode"; import { PackageNode } from "../explorer/models/packageNode"; import { RoutineNode } from "../explorer/models/routineNode"; -import { NodeBase } from "../explorer/models/nodeBase"; import { importAndCompile } from "./compile"; import { ProjectNode } from "../explorer/models/projectNode"; import { openCustomEditors } from "../providers/RuleEditorProvider"; @@ -72,18 +71,17 @@ export class StudioActions { private uri: vscode.Uri; private api: AtelierAPI; private name: string; + public projectEditAnswer?: string; public constructor(uriOrNode?: vscode.Uri | PackageNode | ClassNode | RoutineNode) { if (uriOrNode instanceof vscode.Uri) { - const uri: vscode.Uri = uriOrNode; - this.uri = uri; - this.name = getServerName(uri); - this.api = new AtelierAPI(uri); + this.uri = uriOrNode; + this.name = getServerName(uriOrNode); + this.api = new AtelierAPI(uriOrNode); } else if (uriOrNode) { - const node: NodeBase = uriOrNode; - this.api = new AtelierAPI(node.workspaceFolder); - this.api.setNamespace(node.namespace); - this.name = node instanceof PackageNode ? node.fullName + ".PKG" : node.fullName; + this.api = new AtelierAPI(uriOrNode.workspaceFolderUri || uriOrNode.workspaceFolder); + this.api.setNamespace(uriOrNode.namespace); + this.name = uriOrNode instanceof PackageNode ? uriOrNode.fullName + ".PKG" : uriOrNode.fullName; } else { this.api = new AtelierAPI(); } @@ -105,6 +103,22 @@ export class StudioActions { ); } + /** Fire UserAction `id` on server `api` for project `name`. */ + public async fireProjectUserAction(api: AtelierAPI, name: string, id: OtherStudioAction): Promise { + this.api = api; + this.name = `${name}.PRJ`; + return this.userAction( + { + id: id.toString(), + label: getOtherStudioActionLabel(id), + }, + false, + "", + "", + 1 + ); + } + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types public processUserAction(userAction): Thenable { const serverAction = parseInt(userAction.action || 0, 10); @@ -318,8 +332,14 @@ export class StudioActions { const attemptedEditLabel = getOtherStudioActionLabel(OtherStudioAction.AttemptedEdit); if (afterUserAction && actionToProcess.errorText !== "") { if (action.label === attemptedEditLabel) { - suppressEditListenerMap.set(this.uri.toString(), true); - await vscode.commands.executeCommand("workbench.action.files.revert", this.uri); + if (this.name.toUpperCase().endsWith(".PRJ")) { + // Store the "answer" so the caller knows there was an error + this.projectEditAnswer = "-1"; + } else if (this.uri) { + // Only revert if we have a URI + suppressEditListenerMap.set(this.uri.toString(), true); + await vscode.commands.executeCommand("workbench.action.files.revert", this.uri); + } } outputChannel.appendLine(actionToProcess.errorText); outputChannel.show(); @@ -327,9 +347,16 @@ export class StudioActions { if (actionToProcess && !afterUserAction) { const answer = await this.processUserAction(actionToProcess); // call AfterUserAction only if there is a valid answer - if (action.label === attemptedEditLabel && answer !== "1") { - suppressEditListenerMap.set(this.uri.toString(), true); - await vscode.commands.executeCommand("workbench.action.files.revert", this.uri); + if (action.label === attemptedEditLabel) { + if (answer != "1" && this.uri) { + // Only revert if we have a URI + suppressEditListenerMap.set(this.uri.toString(), true); + await vscode.commands.executeCommand("workbench.action.files.revert", this.uri); + } + if (this.name.toUpperCase().endsWith(".PRJ")) { + // Store the answer. No answer means "allow the edit". + this.projectEditAnswer = answer ?? "1"; + } } if (answer) { answer.msg || answer.msg === "" diff --git a/src/commands/unitTest.ts b/src/commands/unitTest.ts index ea5f2d45..7d736fcf 100644 --- a/src/commands/unitTest.ts +++ b/src/commands/unitTest.ts @@ -5,6 +5,7 @@ import { getFileText, methodOffsetToLine, outputChannel, stripClassMemberNameQuo import { fileSpecFromURI } from "../utils/FileProviderUtil"; import { AtelierAPI } from "../api"; import { DocumentContentProvider } from "../providers/DocumentContentProvider"; +import { StudioActions, OtherStudioAction } from "./studio"; enum TestStatus { Failed = 0, @@ -259,7 +260,7 @@ function replaceRootTestItems(testController: vscode.TestController): void { } /** Create a `Promise` that resolves to a query result containing an array of children for `item`. */ -function childrenForServerSideFolderItem( +async function childrenForServerSideFolderItem( item: vscode.TestItem ): Promise>> { let query: string; @@ -275,6 +276,12 @@ function childrenForServerSideFolderItem( const params = new URLSearchParams(item.uri.query); const api = new AtelierAPI(item.uri); if (params.has("project")) { + // Technically a project is a "document", so tell the server that we're opening it + await new StudioActions() + .fireProjectUserAction(api, params.get("project"), OtherStudioAction.OpenedDocument) + .catch(() => { + // Swallow error because showing it is more disruptive than using a potentially outdated project definition + }); query = "SELECT DISTINCT CASE " + "WHEN $LENGTH(SUBSTR(Name,?),'.') > 1 THEN $PIECE(SUBSTR(Name,?),'.') " + diff --git a/src/explorer/explorer.ts b/src/explorer/explorer.ts index 76684c25..cad90a62 100644 --- a/src/explorer/explorer.ts +++ b/src/explorer/explorer.ts @@ -6,6 +6,7 @@ import { config, OBJECTSCRIPT_FILE_SCHEMA, projectsExplorerProvider } from "../e import { WorkspaceNode } from "./models/workspaceNode"; import { outputChannel } from "../utils"; import { DocumentContentProvider } from "../providers/DocumentContentProvider"; +import { StudioActions, OtherStudioAction } from "../commands/studio"; /** Get the URI for this leaf node */ export function getLeafNodeUri(node: NodeBase, forceServerCopy = false): vscode.Uri { @@ -74,6 +75,20 @@ export function registerExplorerOpen(): vscode.Disposable { if (remove == "Yes") { const api = new AtelierAPI(uri); try { + // Technically a project is a "document", so tell the server that we're editing it + const studioActions = new StudioActions(); + await studioActions.fireProjectUserAction(api, project, OtherStudioAction.AttemptedEdit); + if (studioActions.projectEditAnswer != "1") { + // Don't perform the edit + if (studioActions.projectEditAnswer == "-1") { + // Source control action failed + vscode.window.showErrorMessage( + `'AttemptedEdit' source control action failed for project '${project}'. Check the 'ObjectScript' Output channel for details.`, + "Dismiss" + ); + } + return; + } // Remove the item from the project let prjFileName = fullName.startsWith("/") ? fullName.slice(1) : fullName; const ext = prjFileName.split(".").pop().toLowerCase(); diff --git a/src/explorer/models/projectNode.ts b/src/explorer/models/projectNode.ts index 9823c489..4e344df9 100644 --- a/src/explorer/models/projectNode.ts +++ b/src/explorer/models/projectNode.ts @@ -1,6 +1,8 @@ import * as vscode from "vscode"; import { NodeBase, NodeOptions } from "./nodeBase"; import { ProjectRootNode } from "./projectRootNode"; +import { OtherStudioAction, StudioActions } from "../../commands/studio"; +import { AtelierAPI } from "../../api"; export class ProjectNode extends NodeBase { private description: string; @@ -13,6 +15,13 @@ export class ProjectNode extends NodeBase { const children = []; let node: ProjectRootNode; + // Technically a project is a "document", so tell the server that we're opening it + const api = new AtelierAPI(this.workspaceFolderUri); + api.setNamespace(this.namespace); + await new StudioActions().fireProjectUserAction(api, this.label, OtherStudioAction.OpenedDocument).catch(() => { + // Swallow error because showing it is more disruptive than using a potentially outdated project definition + }); + node = new ProjectRootNode( "Classes", "", diff --git a/src/providers/FileSystemProvider/FileSearchProvider.ts b/src/providers/FileSystemProvider/FileSearchProvider.ts index efa279f7..b5eee054 100644 --- a/src/providers/FileSystemProvider/FileSearchProvider.ts +++ b/src/providers/FileSystemProvider/FileSearchProvider.ts @@ -3,6 +3,8 @@ import { projectContentsFromUri, studioOpenDialogFromURI } from "../../utils/Fil import { notNull } from "../../utils"; import { DocumentContentProvider } from "../DocumentContentProvider"; import { ProjectItem } from "../../commands/project"; +import { StudioActions, OtherStudioAction } from "../../commands/studio"; +import { AtelierAPI } from "../../api"; export class FileSearchProvider implements vscode.FileSearchProvider { /** @@ -11,20 +13,27 @@ export class FileSearchProvider implements vscode.FileSearchProvider { * @param options A set of options to consider while searching files. * @param token A cancellation token. */ - public provideFileSearchResults( + public async provideFileSearchResults( query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken - ): vscode.ProviderResult { + ): Promise { let counter = 0; let pattern = query.pattern.charAt(0) == "/" ? query.pattern.slice(1) : query.pattern; const params = new URLSearchParams(options.folder.query); const csp = params.has("csp") && ["", "1"].includes(params.get("csp")); if (params.has("project") && params.get("project").length) { - const patternRegex = new RegExp(`.*${pattern}.*`.replace(/\.|\//g, "[./]"), "i"); + // Technically a project is a "document", so tell the server that we're opening it + await new StudioActions() + .fireProjectUserAction(new AtelierAPI(options.folder), params.get("project"), OtherStudioAction.OpenedDocument) + .catch(() => { + // Swallow error because showing it is more disruptive than using a potentially outdated project definition + }); if (token.isCancellationRequested) { return; } + + const patternRegex = new RegExp(`.*${pattern}.*`.replace(/\.|\//g, "[./]"), "i"); return projectContentsFromUri(options.folder, true).then((docs) => docs .map((doc: ProjectItem) => { diff --git a/src/providers/FileSystemProvider/FileSystemProvider.ts b/src/providers/FileSystemProvider/FileSystemProvider.ts index dd140ab8..9bee8bb6 100644 --- a/src/providers/FileSystemProvider/FileSystemProvider.ts +++ b/src/providers/FileSystemProvider/FileSystemProvider.ts @@ -3,7 +3,7 @@ import * as vscode from "vscode"; import { AtelierAPI } from "../../api"; import { Directory } from "./Directory"; import { File } from "./File"; -import { fireOtherStudioAction, OtherStudioAction } from "../../commands/studio"; +import { fireOtherStudioAction, OtherStudioAction, StudioActions } from "../../commands/studio"; import { projectContentsFromUri, studioOpenDialogFromURI } from "../../utils/FileProviderUtil"; import { classNameRegex, @@ -202,6 +202,15 @@ export class FileSystemProvider implements vscode.FileSystemProvider { } const params = new URLSearchParams(uri.query); if (params.has("project") && params.get("project").length) { + if (["", "/"].includes(uri.path)) { + // Technically a project is a "document", so tell the server that we're opening it + await new StudioActions() + .fireProjectUserAction(api, params.get("project"), OtherStudioAction.OpenedDocument) + .catch(() => { + // Swallow error because showing it is more disruptive than using a potentially outdated project definition + }); + } + // Get all items in the project return projectContentsFromUri(uri).then((entries) => entries.map((entry) => { diff --git a/src/providers/FileSystemProvider/TextSearchProvider.ts b/src/providers/FileSystemProvider/TextSearchProvider.ts index 98979157..5b58ea20 100644 --- a/src/providers/FileSystemProvider/TextSearchProvider.ts +++ b/src/providers/FileSystemProvider/TextSearchProvider.ts @@ -6,6 +6,7 @@ import { DocumentContentProvider } from "../DocumentContentProvider"; import { notNull, outputChannel, throttleRequests } from "../../utils"; import { config } from "../../extension"; import { fileSpecFromURI } from "../../utils/FileProviderUtil"; +import { OtherStudioAction, StudioActions } from "../../commands/studio"; /** * Convert an `attrline` in a description to a line number in document `content`. @@ -395,6 +396,18 @@ export class TextSearchProvider implements vscode.TextSearchProvider { // Needed because the server matches the full line against the regex and ignores the case parameter when in regex mode const pattern = query.isRegExp ? `${!query.isCaseSensitive ? "(?i)" : ""}.*${query.pattern}.*` : query.pattern; + if (params.has("project") && params.get("project").length) { + // Technically a project is a "document", so tell the server that we're opening it + await new StudioActions() + .fireProjectUserAction(api, params.get("project"), OtherStudioAction.OpenedDocument) + .catch(() => { + // Swallow error because showing it is more disruptive than using a potentially outdated project definition + }); + } + if (token.isCancellationRequested) { + return; + } + if (api.config.apiVersion >= 6) { // Build the request object const project = params.has("project") && params.get("project").length ? params.get("project") : undefined; From 4abf6229d4f8e0bac4c7ca8bb2dc3f4a87c6e3d5 Mon Sep 17 00:00:00 2001 From: Brett Saviano Date: Thu, 22 Feb 2024 07:37:34 -0500 Subject: [PATCH 2/2] Restore auto-closing braces and parentheses (#1316) --- src/languageConfiguration.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/languageConfiguration.ts b/src/languageConfiguration.ts index 0c3f903d..abcbec22 100644 --- a/src/languageConfiguration.ts +++ b/src/languageConfiguration.ts @@ -18,6 +18,21 @@ export function getLanguageConfiguration(lang: string): vscode.LanguageConfigura close: "*/", notIn: [vscode.SyntaxTokenType.Comment, vscode.SyntaxTokenType.String, vscode.SyntaxTokenType.RegEx], }, + { + open: "{", + close: "}", + notIn: [vscode.SyntaxTokenType.Comment, vscode.SyntaxTokenType.String, vscode.SyntaxTokenType.RegEx], + }, + { + open: "(", + close: ")", + notIn: [vscode.SyntaxTokenType.Comment, vscode.SyntaxTokenType.String, vscode.SyntaxTokenType.RegEx], + }, + { + open: '"', + close: '"', + notIn: [vscode.SyntaxTokenType.Comment, vscode.SyntaxTokenType.String, vscode.SyntaxTokenType.RegEx], + }, ], onEnterRules: lang == "objectscript-class"