Skip to content

Commit

Permalink
Fire source control hooks when creating/opening/editing/deleting proj…
Browse files Browse the repository at this point in the history
…ects (#1313)
  • Loading branch information
isc-bsaviano authored Feb 22, 2024
1 parent 792a12e commit 69c684e
Show file tree
Hide file tree
Showing 8 changed files with 176 additions and 20 deletions.
69 changes: 68 additions & 1 deletion src/commands/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -179,6 +195,19 @@ export async function deleteProject(node: ProjectNode | undefined): Promise<any>
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();

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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");
}
}
}
Expand Down Expand Up @@ -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
Expand Down
55 changes: 41 additions & 14 deletions src/commands/studio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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();
}
Expand All @@ -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<void> {
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<any> {
const serverAction = parseInt(userAction.action || 0, 10);
Expand Down Expand Up @@ -318,18 +332,31 @@ 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();
}
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 === ""
Expand Down
9 changes: 8 additions & 1 deletion src/commands/unitTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<Atelier.Response<Atelier.Content<{ Name: string }[]>>> {
let query: string;
Expand All @@ -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,?),'.') " +
Expand Down
15 changes: 15 additions & 0 deletions src/explorer/explorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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();
Expand Down
9 changes: 9 additions & 0 deletions src/explorer/models/projectNode.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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",
"",
Expand Down
15 changes: 12 additions & 3 deletions src/providers/FileSystemProvider/FileSearchProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand All @@ -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<vscode.Uri[]> {
): Promise<vscode.Uri[]> {
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) => {
Expand Down
11 changes: 10 additions & 1 deletion src/providers/FileSystemProvider/FileSystemProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) => {
Expand Down
13 changes: 13 additions & 0 deletions src/providers/FileSystemProvider/TextSearchProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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;
Expand Down

0 comments on commit 69c684e

Please sign in to comment.