Skip to content

Commit 69c684e

Browse files
authored
Fire source control hooks when creating/opening/editing/deleting projects (#1313)
1 parent 792a12e commit 69c684e

File tree

8 files changed

+176
-20
lines changed

8 files changed

+176
-20
lines changed

src/commands/project.ts

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { isCSPFile } from "../providers/FileSystemProvider/FileSystemProvider";
1212
import { notNull, outputChannel } from "../utils";
1313
import { pickServerAndNamespace } from "./addServerNamespaceToWorkspace";
1414
import { exportList } from "./export";
15+
import { OtherStudioAction, StudioActions } from "./studio";
1516

1617
export interface ProjectItem {
1718
Name: string;
@@ -137,6 +138,21 @@ export async function createProject(node: NodeBase | undefined, api?: AtelierAPI
137138
return;
138139
}
139140

141+
// Technically a project is a "document", so tell the server that we created it
142+
try {
143+
const studioActions = new StudioActions();
144+
await studioActions.fireProjectUserAction(api, name, OtherStudioAction.CreatedNewDocument);
145+
await studioActions.fireProjectUserAction(api, name, OtherStudioAction.FirstTimeDocumentSave);
146+
} catch (error) {
147+
let message = `Source control actions failed for project '${name}'.`;
148+
if (error && error.errorText && error.errorText !== "") {
149+
outputChannel.appendLine("\n" + error.errorText);
150+
outputChannel.show(true);
151+
message += " Check 'ObjectScript' output channel for details.";
152+
}
153+
vscode.window.showErrorMessage(message, "Dismiss");
154+
}
155+
140156
// Refresh the explorer
141157
projectsExplorerProvider.refresh();
142158

@@ -179,6 +195,19 @@ export async function deleteProject(node: ProjectNode | undefined): Promise<any>
179195
return vscode.window.showErrorMessage(message, "Dismiss");
180196
}
181197

198+
// Technically a project is a "document", so tell the server that we deleted it
199+
try {
200+
await new StudioActions().fireProjectUserAction(api, project, OtherStudioAction.DeletedDocument);
201+
} catch (error) {
202+
let message = `'DeletedDocument' source control action failed for project '${project}'.`;
203+
if (error && error.errorText && error.errorText !== "") {
204+
outputChannel.appendLine("\n" + error.errorText);
205+
outputChannel.show(true);
206+
message += " Check 'ObjectScript' output channel for details.";
207+
}
208+
vscode.window.showErrorMessage(message, "Dismiss");
209+
}
210+
182211
// Refresh the explorer
183212
projectsExplorerProvider.refresh();
184213

@@ -706,6 +735,12 @@ export async function modifyProject(
706735
return;
707736
}
708737
}
738+
739+
// Technically a project is a "document", so tell the server that we're opening it
740+
await new StudioActions().fireProjectUserAction(api, project, OtherStudioAction.OpenedDocument).catch(() => {
741+
// Swallow error because showing it is more disruptive than using a potentially outdated project definition
742+
});
743+
709744
let items: ProjectItem[] = await api
710745
.actionQuery("SELECT Name, Type FROM %Studio.Project_ProjectItemsList(?,?) WHERE Type != 'GBL'", [project, "1"])
711746
.then((data) => data.result.content);
@@ -862,6 +897,23 @@ export async function modifyProject(
862897
}
863898

864899
try {
900+
if (add.length || remove.length) {
901+
// Technically a project is a "document", so tell the server that we're editing it
902+
const studioActions = new StudioActions();
903+
await studioActions.fireProjectUserAction(api, project, OtherStudioAction.AttemptedEdit);
904+
if (studioActions.projectEditAnswer != "1") {
905+
// Don't perform the edit
906+
if (studioActions.projectEditAnswer == "-1") {
907+
// Source control action failed
908+
vscode.window.showErrorMessage(
909+
`'AttemptedEdit' source control action failed for project '${project}'. Check the 'ObjectScript' Output channel for details.`,
910+
"Dismiss"
911+
);
912+
}
913+
return;
914+
}
915+
}
916+
865917
if (remove.length) {
866918
// Delete the obsolete items
867919
await api.actionQuery(
@@ -900,7 +952,7 @@ export async function modifyProject(
900952

901953
// Refresh the files explorer if there's an isfs folder for this project
902954
if (node == undefined && isfsFolderForProject(project, node ?? api.configName) != -1) {
903-
await vscode.commands.executeCommand("workbench.files.action.refreshFilesExplorer");
955+
vscode.commands.executeCommand("workbench.files.action.refreshFilesExplorer");
904956
}
905957
}
906958
}
@@ -1070,6 +1122,21 @@ export async function addIsfsFileToProject(
10701122

10711123
try {
10721124
if (add.length) {
1125+
// Technically a project is a "document", so tell the server that we're editing it
1126+
const studioActions = new StudioActions();
1127+
await studioActions.fireProjectUserAction(api, project, OtherStudioAction.AttemptedEdit);
1128+
if (studioActions.projectEditAnswer != "1") {
1129+
// Don't perform the edit
1130+
if (studioActions.projectEditAnswer == "-1") {
1131+
// Source control action failed
1132+
vscode.window.showErrorMessage(
1133+
`'AttemptedEdit' source control action failed for project '${project}'. Check the 'ObjectScript' Output channel for details.`,
1134+
"Dismiss"
1135+
);
1136+
}
1137+
return;
1138+
}
1139+
10731140
// Add any new items
10741141
await api.actionQuery(
10751142
`INSERT INTO %Studio.ProjectItem (Project,Name,Type) SELECT * FROM (${add

src/commands/studio.ts

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { DocumentContentProvider } from "../providers/DocumentContentProvider";
66
import { ClassNode } from "../explorer/models/classNode";
77
import { PackageNode } from "../explorer/models/packageNode";
88
import { RoutineNode } from "../explorer/models/routineNode";
9-
import { NodeBase } from "../explorer/models/nodeBase";
109
import { importAndCompile } from "./compile";
1110
import { ProjectNode } from "../explorer/models/projectNode";
1211
import { openCustomEditors } from "../providers/RuleEditorProvider";
@@ -72,18 +71,17 @@ export class StudioActions {
7271
private uri: vscode.Uri;
7372
private api: AtelierAPI;
7473
private name: string;
74+
public projectEditAnswer?: string;
7575

7676
public constructor(uriOrNode?: vscode.Uri | PackageNode | ClassNode | RoutineNode) {
7777
if (uriOrNode instanceof vscode.Uri) {
78-
const uri: vscode.Uri = uriOrNode;
79-
this.uri = uri;
80-
this.name = getServerName(uri);
81-
this.api = new AtelierAPI(uri);
78+
this.uri = uriOrNode;
79+
this.name = getServerName(uriOrNode);
80+
this.api = new AtelierAPI(uriOrNode);
8281
} else if (uriOrNode) {
83-
const node: NodeBase = uriOrNode;
84-
this.api = new AtelierAPI(node.workspaceFolder);
85-
this.api.setNamespace(node.namespace);
86-
this.name = node instanceof PackageNode ? node.fullName + ".PKG" : node.fullName;
82+
this.api = new AtelierAPI(uriOrNode.workspaceFolderUri || uriOrNode.workspaceFolder);
83+
this.api.setNamespace(uriOrNode.namespace);
84+
this.name = uriOrNode instanceof PackageNode ? uriOrNode.fullName + ".PKG" : uriOrNode.fullName;
8785
} else {
8886
this.api = new AtelierAPI();
8987
}
@@ -105,6 +103,22 @@ export class StudioActions {
105103
);
106104
}
107105

106+
/** Fire UserAction `id` on server `api` for project `name`. */
107+
public async fireProjectUserAction(api: AtelierAPI, name: string, id: OtherStudioAction): Promise<void> {
108+
this.api = api;
109+
this.name = `${name}.PRJ`;
110+
return this.userAction(
111+
{
112+
id: id.toString(),
113+
label: getOtherStudioActionLabel(id),
114+
},
115+
false,
116+
"",
117+
"",
118+
1
119+
);
120+
}
121+
108122
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
109123
public processUserAction(userAction): Thenable<any> {
110124
const serverAction = parseInt(userAction.action || 0, 10);
@@ -318,18 +332,31 @@ export class StudioActions {
318332
const attemptedEditLabel = getOtherStudioActionLabel(OtherStudioAction.AttemptedEdit);
319333
if (afterUserAction && actionToProcess.errorText !== "") {
320334
if (action.label === attemptedEditLabel) {
321-
suppressEditListenerMap.set(this.uri.toString(), true);
322-
await vscode.commands.executeCommand("workbench.action.files.revert", this.uri);
335+
if (this.name.toUpperCase().endsWith(".PRJ")) {
336+
// Store the "answer" so the caller knows there was an error
337+
this.projectEditAnswer = "-1";
338+
} else if (this.uri) {
339+
// Only revert if we have a URI
340+
suppressEditListenerMap.set(this.uri.toString(), true);
341+
await vscode.commands.executeCommand("workbench.action.files.revert", this.uri);
342+
}
323343
}
324344
outputChannel.appendLine(actionToProcess.errorText);
325345
outputChannel.show();
326346
}
327347
if (actionToProcess && !afterUserAction) {
328348
const answer = await this.processUserAction(actionToProcess);
329349
// call AfterUserAction only if there is a valid answer
330-
if (action.label === attemptedEditLabel && answer !== "1") {
331-
suppressEditListenerMap.set(this.uri.toString(), true);
332-
await vscode.commands.executeCommand("workbench.action.files.revert", this.uri);
350+
if (action.label === attemptedEditLabel) {
351+
if (answer != "1" && this.uri) {
352+
// Only revert if we have a URI
353+
suppressEditListenerMap.set(this.uri.toString(), true);
354+
await vscode.commands.executeCommand("workbench.action.files.revert", this.uri);
355+
}
356+
if (this.name.toUpperCase().endsWith(".PRJ")) {
357+
// Store the answer. No answer means "allow the edit".
358+
this.projectEditAnswer = answer ?? "1";
359+
}
333360
}
334361
if (answer) {
335362
answer.msg || answer.msg === ""

src/commands/unitTest.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { getFileText, methodOffsetToLine, outputChannel, stripClassMemberNameQuo
55
import { fileSpecFromURI } from "../utils/FileProviderUtil";
66
import { AtelierAPI } from "../api";
77
import { DocumentContentProvider } from "../providers/DocumentContentProvider";
8+
import { StudioActions, OtherStudioAction } from "./studio";
89

910
enum TestStatus {
1011
Failed = 0,
@@ -259,7 +260,7 @@ function replaceRootTestItems(testController: vscode.TestController): void {
259260
}
260261

261262
/** Create a `Promise` that resolves to a query result containing an array of children for `item`. */
262-
function childrenForServerSideFolderItem(
263+
async function childrenForServerSideFolderItem(
263264
item: vscode.TestItem
264265
): Promise<Atelier.Response<Atelier.Content<{ Name: string }[]>>> {
265266
let query: string;
@@ -275,6 +276,12 @@ function childrenForServerSideFolderItem(
275276
const params = new URLSearchParams(item.uri.query);
276277
const api = new AtelierAPI(item.uri);
277278
if (params.has("project")) {
279+
// Technically a project is a "document", so tell the server that we're opening it
280+
await new StudioActions()
281+
.fireProjectUserAction(api, params.get("project"), OtherStudioAction.OpenedDocument)
282+
.catch(() => {
283+
// Swallow error because showing it is more disruptive than using a potentially outdated project definition
284+
});
278285
query =
279286
"SELECT DISTINCT CASE " +
280287
"WHEN $LENGTH(SUBSTR(Name,?),'.') > 1 THEN $PIECE(SUBSTR(Name,?),'.') " +

src/explorer/explorer.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { config, OBJECTSCRIPT_FILE_SCHEMA, projectsExplorerProvider } from "../e
66
import { WorkspaceNode } from "./models/workspaceNode";
77
import { outputChannel } from "../utils";
88
import { DocumentContentProvider } from "../providers/DocumentContentProvider";
9+
import { StudioActions, OtherStudioAction } from "../commands/studio";
910

1011
/** Get the URI for this leaf node */
1112
export function getLeafNodeUri(node: NodeBase, forceServerCopy = false): vscode.Uri {
@@ -74,6 +75,20 @@ export function registerExplorerOpen(): vscode.Disposable {
7475
if (remove == "Yes") {
7576
const api = new AtelierAPI(uri);
7677
try {
78+
// Technically a project is a "document", so tell the server that we're editing it
79+
const studioActions = new StudioActions();
80+
await studioActions.fireProjectUserAction(api, project, OtherStudioAction.AttemptedEdit);
81+
if (studioActions.projectEditAnswer != "1") {
82+
// Don't perform the edit
83+
if (studioActions.projectEditAnswer == "-1") {
84+
// Source control action failed
85+
vscode.window.showErrorMessage(
86+
`'AttemptedEdit' source control action failed for project '${project}'. Check the 'ObjectScript' Output channel for details.`,
87+
"Dismiss"
88+
);
89+
}
90+
return;
91+
}
7792
// Remove the item from the project
7893
let prjFileName = fullName.startsWith("/") ? fullName.slice(1) : fullName;
7994
const ext = prjFileName.split(".").pop().toLowerCase();

src/explorer/models/projectNode.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import * as vscode from "vscode";
22
import { NodeBase, NodeOptions } from "./nodeBase";
33
import { ProjectRootNode } from "./projectRootNode";
4+
import { OtherStudioAction, StudioActions } from "../../commands/studio";
5+
import { AtelierAPI } from "../../api";
46

57
export class ProjectNode extends NodeBase {
68
private description: string;
@@ -13,6 +15,13 @@ export class ProjectNode extends NodeBase {
1315
const children = [];
1416
let node: ProjectRootNode;
1517

18+
// Technically a project is a "document", so tell the server that we're opening it
19+
const api = new AtelierAPI(this.workspaceFolderUri);
20+
api.setNamespace(this.namespace);
21+
await new StudioActions().fireProjectUserAction(api, this.label, OtherStudioAction.OpenedDocument).catch(() => {
22+
// Swallow error because showing it is more disruptive than using a potentially outdated project definition
23+
});
24+
1625
node = new ProjectRootNode(
1726
"Classes",
1827
"",

src/providers/FileSystemProvider/FileSearchProvider.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { projectContentsFromUri, studioOpenDialogFromURI } from "../../utils/Fil
33
import { notNull } from "../../utils";
44
import { DocumentContentProvider } from "../DocumentContentProvider";
55
import { ProjectItem } from "../../commands/project";
6+
import { StudioActions, OtherStudioAction } from "../../commands/studio";
7+
import { AtelierAPI } from "../../api";
68

79
export class FileSearchProvider implements vscode.FileSearchProvider {
810
/**
@@ -11,20 +13,27 @@ export class FileSearchProvider implements vscode.FileSearchProvider {
1113
* @param options A set of options to consider while searching files.
1214
* @param token A cancellation token.
1315
*/
14-
public provideFileSearchResults(
16+
public async provideFileSearchResults(
1517
query: vscode.FileSearchQuery,
1618
options: vscode.FileSearchOptions,
1719
token: vscode.CancellationToken
18-
): vscode.ProviderResult<vscode.Uri[]> {
20+
): Promise<vscode.Uri[]> {
1921
let counter = 0;
2022
let pattern = query.pattern.charAt(0) == "/" ? query.pattern.slice(1) : query.pattern;
2123
const params = new URLSearchParams(options.folder.query);
2224
const csp = params.has("csp") && ["", "1"].includes(params.get("csp"));
2325
if (params.has("project") && params.get("project").length) {
24-
const patternRegex = new RegExp(`.*${pattern}.*`.replace(/\.|\//g, "[./]"), "i");
26+
// Technically a project is a "document", so tell the server that we're opening it
27+
await new StudioActions()
28+
.fireProjectUserAction(new AtelierAPI(options.folder), params.get("project"), OtherStudioAction.OpenedDocument)
29+
.catch(() => {
30+
// Swallow error because showing it is more disruptive than using a potentially outdated project definition
31+
});
2532
if (token.isCancellationRequested) {
2633
return;
2734
}
35+
36+
const patternRegex = new RegExp(`.*${pattern}.*`.replace(/\.|\//g, "[./]"), "i");
2837
return projectContentsFromUri(options.folder, true).then((docs) =>
2938
docs
3039
.map((doc: ProjectItem) => {

src/providers/FileSystemProvider/FileSystemProvider.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as vscode from "vscode";
33
import { AtelierAPI } from "../../api";
44
import { Directory } from "./Directory";
55
import { File } from "./File";
6-
import { fireOtherStudioAction, OtherStudioAction } from "../../commands/studio";
6+
import { fireOtherStudioAction, OtherStudioAction, StudioActions } from "../../commands/studio";
77
import { projectContentsFromUri, studioOpenDialogFromURI } from "../../utils/FileProviderUtil";
88
import {
99
classNameRegex,
@@ -202,6 +202,15 @@ export class FileSystemProvider implements vscode.FileSystemProvider {
202202
}
203203
const params = new URLSearchParams(uri.query);
204204
if (params.has("project") && params.get("project").length) {
205+
if (["", "/"].includes(uri.path)) {
206+
// Technically a project is a "document", so tell the server that we're opening it
207+
await new StudioActions()
208+
.fireProjectUserAction(api, params.get("project"), OtherStudioAction.OpenedDocument)
209+
.catch(() => {
210+
// Swallow error because showing it is more disruptive than using a potentially outdated project definition
211+
});
212+
}
213+
205214
// Get all items in the project
206215
return projectContentsFromUri(uri).then((entries) =>
207216
entries.map((entry) => {

src/providers/FileSystemProvider/TextSearchProvider.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { DocumentContentProvider } from "../DocumentContentProvider";
66
import { notNull, outputChannel, throttleRequests } from "../../utils";
77
import { config } from "../../extension";
88
import { fileSpecFromURI } from "../../utils/FileProviderUtil";
9+
import { OtherStudioAction, StudioActions } from "../../commands/studio";
910

1011
/**
1112
* Convert an `attrline` in a description to a line number in document `content`.
@@ -395,6 +396,18 @@ export class TextSearchProvider implements vscode.TextSearchProvider {
395396
// Needed because the server matches the full line against the regex and ignores the case parameter when in regex mode
396397
const pattern = query.isRegExp ? `${!query.isCaseSensitive ? "(?i)" : ""}.*${query.pattern}.*` : query.pattern;
397398

399+
if (params.has("project") && params.get("project").length) {
400+
// Technically a project is a "document", so tell the server that we're opening it
401+
await new StudioActions()
402+
.fireProjectUserAction(api, params.get("project"), OtherStudioAction.OpenedDocument)
403+
.catch(() => {
404+
// Swallow error because showing it is more disruptive than using a potentially outdated project definition
405+
});
406+
}
407+
if (token.isCancellationRequested) {
408+
return;
409+
}
410+
398411
if (api.config.apiVersion >= 6) {
399412
// Build the request object
400413
const project = params.has("project") && params.get("project").length ? params.get("project") : undefined;

0 commit comments

Comments
 (0)