Skip to content

Fire source control hooks when creating/opening/editing/deleting projects #1313

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Feb 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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