Skip to content

Commit 24aa75f

Browse files
authored
Add command for importing local files into server-side workspace folder (#1036)
1 parent 870d453 commit 24aa75f

File tree

4 files changed

+256
-5
lines changed

4 files changed

+256
-5
lines changed

package.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,10 @@
319319
{
320320
"command": "vscode-objectscript.newFile.dtl",
321321
"when": "false"
322+
},
323+
{
324+
"command": "vscode-objectscript.importLocalFilesServerSide",
325+
"when": "false"
322326
}
323327
],
324328
"view/title": [
@@ -563,6 +567,11 @@
563567
"command": "vscode-objectscript.removeItemsFromProject",
564568
"when": "vscode-objectscript.connectActive && resourceScheme =~ /^isfs(-readonly)?$/ && resource =~ /project%3D/ && explorerResourceIsRoot",
565569
"group": "objectscript_prj@2"
570+
},
571+
{
572+
"command": "vscode-objectscript.importLocalFilesServerSide",
573+
"when": "vscode-objectscript.connectActive && resourceScheme == isfs && explorerResourceIsRoot",
574+
"group": "objectscript_modify@2"
566575
}
567576
],
568577
"file/newFile": [
@@ -1050,6 +1059,11 @@
10501059
"category": "ObjectScript",
10511060
"command": "vscode-objectscript.newFile.dtl",
10521061
"title": "Data Transformation"
1062+
},
1063+
{
1064+
"category": "ObjectScript",
1065+
"command": "vscode-objectscript.importLocalFilesServerSide",
1066+
"title": "Import Local Files..."
10531067
}
10541068
],
10551069
"keybindings": [

src/commands/compile.ts

Lines changed: 226 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
currentFile,
1818
CurrentFile,
1919
currentFileFromContent,
20+
notNull,
2021
outputChannel,
2122
throttleRequests,
2223
} from "../utils";
@@ -69,12 +70,12 @@ export async function checkChangedOnServer(file: CurrentFile, force = false): Pr
6970
return mtime;
7071
}
7172

72-
async function importFile(file: CurrentFile, ignoreConflict?: boolean): Promise<any> {
73+
async function importFile(file: CurrentFile, ignoreConflict?: boolean, skipDeplCheck = false): Promise<any> {
7374
const api = new AtelierAPI(file.uri);
74-
if (file.name.split(".").pop().toLowerCase() === "cls") {
75+
if (file.name.split(".").pop().toLowerCase() === "cls" && !skipDeplCheck) {
7576
const result = await api.actionIndex([file.name]);
7677
if (result.result.content[0].content.depl) {
77-
vscode.window.showErrorMessage("Cannot import over a deployed class");
78+
vscode.window.showErrorMessage(`Cannot import ${file.name} because it is deployed on the server.`, "Dismiss");
7879
return Promise.reject();
7980
}
8081
}
@@ -126,7 +127,7 @@ What do you want to do?`,
126127
// Clear cache entry
127128
workspaceState.update(`${file.uniqueId}:mtime`, undefined);
128129
// Overwrite
129-
return importFile(file, true);
130+
return importFile(file, true, true);
130131
case "Pull Server Changes":
131132
outputChannel.appendLine(`${file.name}: Loading changes from server`);
132133
outputChannel.show(true);
@@ -499,3 +500,224 @@ export async function compileExplorerItems(nodes: NodeBase[]): Promise<any> {
499500
})
500501
);
501502
}
503+
504+
/** Import file `name` to server `api`. Used for importing local files that are not used as part of a client-side editing workspace. */
505+
async function importFileFromContent(
506+
name: string,
507+
content: string,
508+
api: AtelierAPI,
509+
ignoreConflict?: boolean,
510+
skipDeplCheck = false
511+
): Promise<void> {
512+
if (name.split(".").pop().toLowerCase() === "cls" && !skipDeplCheck) {
513+
const result = await api.actionIndex([name]);
514+
if (result.result.content[0].content.depl) {
515+
vscode.window.showErrorMessage(`Cannot import ${name} because it is deployed on the server.`, "Dismiss");
516+
return Promise.reject();
517+
}
518+
}
519+
ignoreConflict = ignoreConflict || config("overwriteServerChanges");
520+
return api
521+
.putDoc(
522+
name,
523+
{
524+
content: content.split(/\r?\n/),
525+
enc: false,
526+
// We don't have an mtime for this file because it's outside a local workspace folder
527+
mtime: 0,
528+
},
529+
ignoreConflict
530+
)
531+
.then(() => {
532+
return;
533+
})
534+
.catch((error) => {
535+
if (error?.statusCode == 409) {
536+
return vscode.window
537+
.showErrorMessage(
538+
`Failed to import '${name}' because it already exists on the server. Overwrite server copy?`,
539+
"Yes",
540+
"No"
541+
)
542+
.then((action) => {
543+
if (action == "Yes") {
544+
return importFileFromContent(name, content, api, true, true);
545+
} else {
546+
return Promise.reject();
547+
}
548+
});
549+
} else {
550+
if (error && error.errorText && error.errorText !== "") {
551+
outputChannel.appendLine("\n" + error.errorText);
552+
vscode.window
553+
.showErrorMessage(
554+
`Failed to save file '${name}' on the server. Check 'ObjectScript' output channel for details.`,
555+
"Show",
556+
"Dismiss"
557+
)
558+
.then((action) => {
559+
if (action === "Show") {
560+
outputChannel.show(true);
561+
}
562+
});
563+
} else {
564+
vscode.window.showErrorMessage(`Failed to save file '${name}' on the server.`, "Dismiss");
565+
}
566+
return Promise.reject();
567+
}
568+
});
569+
}
570+
571+
/** Import files from the local file system into a server-namespace from an `isfs` workspace folder. */
572+
export async function importLocalFilesToServerSideFolder(wsFolderUri: vscode.Uri): Promise<any> {
573+
if (
574+
!(
575+
wsFolderUri instanceof vscode.Uri &&
576+
wsFolderUri.scheme == FILESYSTEM_SCHEMA &&
577+
(vscode.workspace.workspaceFolders != undefined
578+
? vscode.workspace.workspaceFolders.findIndex(
579+
(wsFolder) => wsFolder.uri.toString() == wsFolderUri.toString()
580+
) != -1
581+
: false)
582+
)
583+
) {
584+
// Need an isfs workspace folder URI
585+
return;
586+
}
587+
if (vscode.workspace.workspaceFile.scheme != "file") {
588+
vscode.window.showErrorMessage(
589+
"'Import Local Files...' command is not supported for unsaved workspaces.",
590+
"Dismiss"
591+
);
592+
return;
593+
}
594+
const api = new AtelierAPI(wsFolderUri);
595+
// Prompt the user for files to import
596+
let uris = await vscode.window.showOpenDialog({
597+
canSelectFiles: true,
598+
canSelectFolders: false,
599+
canSelectMany: true,
600+
openLabel: "Import",
601+
filters: {
602+
"InterSystems Files": ["cls", "mac", "int", "inc"],
603+
},
604+
// Need a default URI with file scheme or the open dialog
605+
// will show the virtual files from the workspace folder
606+
defaultUri: vscode.workspace.workspaceFile,
607+
});
608+
if (!Array.isArray(uris) || uris.length == 0) {
609+
// No files to import
610+
return;
611+
}
612+
// Filter out non-ISC files
613+
uris = uris.filter((uri) => ["cls", "mac", "int", "inc"].includes(uri.path.split(".").pop().toLowerCase()));
614+
if (uris.length == 0) {
615+
vscode.window.showErrorMessage("No classes or routines were selected.", "Dismiss");
616+
return;
617+
}
618+
// Import the files
619+
return Promise.allSettled<string>(
620+
uris.map(
621+
throttleRequests((uri: vscode.Uri) =>
622+
vscode.workspace.fs
623+
.readFile(uri)
624+
.then((contentBytes) => new TextDecoder().decode(contentBytes))
625+
.then((content) => {
626+
// Determine the name of this file
627+
let docName = "";
628+
let ext = "";
629+
if (uri.path.split(".").pop().toLowerCase() == "cls") {
630+
// Allow Unicode letters
631+
const match = content.match(/^[ \t]*Class[ \t]+(%?[\p{L}\d]+(?:\.[\p{L}\d]+)+)/imu);
632+
if (match) {
633+
[, docName, ext = "cls"] = match;
634+
}
635+
} else {
636+
const match = content.match(/^ROUTINE ([^\s]+)(?:\s*\[\s*Type\s*=\s*\b([a-z]{3})\b)?/i);
637+
if (match) {
638+
[, docName, ext = "mac"] = match;
639+
} else {
640+
const basename = uri.path.split("/").pop();
641+
docName = basename.slice(0, basename.lastIndexOf("."));
642+
ext = basename.slice(basename.lastIndexOf(".") + 1);
643+
}
644+
}
645+
if (docName != "" && ext != "") {
646+
docName += `.${ext.toLowerCase()}`;
647+
return importFileFromContent(docName, content, api).then(() => {
648+
outputChannel.appendLine("Imported file: " + uri.path.split("/").pop());
649+
return docName;
650+
});
651+
} else {
652+
vscode.window.showErrorMessage(
653+
`Cannot determine document name for file ${uri.toString(true)}.`,
654+
"Dismiss"
655+
);
656+
return Promise.reject();
657+
}
658+
})
659+
)
660+
)
661+
).then((results) => {
662+
const imported = results.map((result) => (result.status == "fulfilled" ? result.value : null)).filter(notNull);
663+
// Prompt the user for compilation
664+
if (imported.length) {
665+
return vscode.window
666+
.showInformationMessage(
667+
`Imported ${imported.length == 1 ? imported[0] : `${imported.length} files`}. Compile ${
668+
imported.length > 1 ? "them" : "it"
669+
}?`,
670+
"Yes",
671+
"No"
672+
)
673+
.then((response) => {
674+
if (response == "Yes") {
675+
// Compile the imported files
676+
return vscode.window.withProgress(
677+
{
678+
cancellable: true,
679+
location: vscode.ProgressLocation.Notification,
680+
title: `Compiling: ${imported.length == 1 ? imported[0] : imported.length + " files"}`,
681+
},
682+
(progress, token: vscode.CancellationToken) =>
683+
api
684+
.asyncCompile(imported, token, config("compileFlags"))
685+
.then((data) => {
686+
const info = imported.length > 1 ? "" : `${imported[0]}: `;
687+
if (data.status && data.status.errors && data.status.errors.length) {
688+
throw new Error(`${info}Compile error`);
689+
} else if (!config("suppressCompileMessages")) {
690+
vscode.window.showInformationMessage(`${info}Compilation succeeded.`, "Dismiss");
691+
}
692+
})
693+
.catch(() => {
694+
if (!config("suppressCompileErrorMessages")) {
695+
vscode.window
696+
.showErrorMessage(
697+
"Compilation failed. Check 'ObjectScript' output channel for details.",
698+
"Show",
699+
"Dismiss"
700+
)
701+
.then((action) => {
702+
if (action === "Show") {
703+
outputChannel.show(true);
704+
}
705+
});
706+
}
707+
})
708+
.finally(() => {
709+
// Refresh the files explorer to show the new files
710+
vscode.commands.executeCommand("workbench.files.action.refreshFilesExplorer");
711+
})
712+
);
713+
} else {
714+
// Refresh the files explorer to show the new files
715+
vscode.commands.executeCommand("workbench.files.action.refreshFilesExplorer");
716+
return Promise.resolve();
717+
}
718+
});
719+
} else {
720+
return Promise.resolve();
721+
}
722+
});
723+
}

src/commands/newFile.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ export async function newFile(type: NewFileType): Promise<void> {
233233
try {
234234
// Select a workspace folder
235235
let wsFolder: vscode.WorkspaceFolder;
236-
if (vscode.workspace.workspaceFolders.length == 0) {
236+
if (vscode.workspace.workspaceFolders == undefined || vscode.workspace.workspaceFolders.length == 0) {
237237
vscode.window.showErrorMessage("No workspace folders are open.", "Dismiss");
238238
return;
239239
} else if (vscode.workspace.workspaceFolders.length == 1) {

src/extension.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
compileExplorerItems,
2626
checkChangedOnServer,
2727
compileOnly,
28+
importLocalFilesToServerSideFolder,
2829
} from "./commands/compile";
2930
import { deleteExplorerItems } from "./commands/delete";
3031
import { exportAll, exportCurrentFile, exportExplorerItems, getCategory } from "./commands/export";
@@ -1181,6 +1182,20 @@ export async function activate(context: vscode.ExtensionContext): Promise<any> {
11811182
newFile(NewFileType.BusinessService)
11821183
),
11831184
vscode.commands.registerCommand("vscode-objectscript.newFile.dtl", () => newFile(NewFileType.DTL)),
1185+
vscode.commands.registerCommand("vscode-objectscript.importLocalFilesServerSide", (wsFolderUri) => {
1186+
if (
1187+
wsFolderUri instanceof vscode.Uri &&
1188+
wsFolderUri.scheme == FILESYSTEM_SCHEMA &&
1189+
(vscode.workspace.workspaceFolders != undefined
1190+
? vscode.workspace.workspaceFolders.findIndex(
1191+
(wsFolder) => wsFolder.uri.toString() == wsFolderUri.toString()
1192+
) != -1
1193+
: false)
1194+
) {
1195+
// wsFolderUri is an isfs workspace folder URI
1196+
return importLocalFilesToServerSideFolder(wsFolderUri);
1197+
}
1198+
}),
11841199

11851200
/* Anything we use from the VS Code proposed API */
11861201
...proposed

0 commit comments

Comments
 (0)