diff --git a/package.json b/package.json index d3ae319c..d26139e4 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ } ], "engines": { - "vscode": "^1.43.0" + "vscode": "^1.59.0" }, "enableProposedApi": true, "activationEvents": [ @@ -1059,6 +1059,21 @@ ] } ], + "customEditors": [ + { + "viewType": "vscode-objectscript.bplDtlEditor", + "displayName": "BPL Editor", + "selector": [ + { + "filenamePattern": "*.bpl" + }, + { + "filenamePattern": "*.dtl" + } + ], + "priority": "default" + } + ], "resourceLabelFormatters": [ { "scheme": "isfs", diff --git a/src/commands/viewOthers.ts b/src/commands/viewOthers.ts index 555f1299..7e6dbb39 100644 --- a/src/commands/viewOthers.ts +++ b/src/commands/viewOthers.ts @@ -1,12 +1,18 @@ import * as vscode from "vscode"; import { AtelierAPI } from "../api"; import { config } from "../extension"; +import { currentBplDtlClassDoc } from "../providers/bplDtlEditor"; import { DocumentContentProvider } from "../providers/DocumentContentProvider"; import { currentFile, outputChannel } from "../utils"; +import { BplDtlEditorProvider } from "../providers/bplDtlEditor"; export async function viewOthers(forceEditable = false): Promise { const file = currentFile(); if (!file) { + // BPL/DTL files are not supported for the standard view other method + if (currentBplDtlClassDoc) { + vscode.window.showTextDocument(currentBplDtlClassDoc); + } return; } if (file.uri.scheme === "file" && !config("conn").active) { @@ -77,7 +83,11 @@ export async function viewOthers(forceEditable = false): Promise { const linenum: number = +loc.slice(1); options.selection = new vscode.Range(linenum, 0, linenum, 0); } - vscode.window.showTextDocument(uri, options); + if (item.endsWith(".bpl") || item.endsWith("dtl")) { + vscode.commands.executeCommand("vscode.openWith", uri, BplDtlEditorProvider.viewType); + } else { + vscode.window.showTextDocument(uri, options); + } } else { let uri: vscode.Uri; if (forceEditable) { @@ -85,7 +95,11 @@ export async function viewOthers(forceEditable = false): Promise { } else { uri = DocumentContentProvider.getUri(item); } - vscode.window.showTextDocument(uri); + if (item.endsWith(".bpl") || item.endsWith("dtl")) { + vscode.commands.executeCommand("vscode.openWith", uri, BplDtlEditorProvider.viewType); + } else { + vscode.window.showTextDocument(uri); + } } }; diff --git a/src/extension.ts b/src/extension.ts index 5e760963..df648001 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -98,6 +98,7 @@ export let xmlContentProvider: XmlContentProvider; import TelemetryReporter from "vscode-extension-telemetry"; import { CodeActionProvider } from "./providers/CodeActionProvider"; +import { BplDtlEditorProvider } from "./providers/bplDtlEditor"; const packageJson = vscode.extensions.getExtension(extensionId).packageJSON; const extensionVersion = packageJson.version; @@ -578,7 +579,8 @@ export async function activate(context: vscode.ExtensionContext): Promise { workspace.onDidSaveTextDocument((file) => { if (schemas.includes(file.uri.scheme) || languages.includes(file.languageId)) { - if (documentBeingProcessed !== file) { + const fileExt = file.fileName.split(".").pop(); + if (documentBeingProcessed !== file && !["bpl", "dtl"].includes(fileExt)) { return importAndCompile(false, file, config("compileOnSave")); } } else if (file.uri.scheme === "file") { @@ -921,6 +923,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { new DocumentLinkProvider() ), vscode.commands.registerCommand("vscode-objectscript.editOthers", () => viewOthers(true)), + BplDtlEditorProvider.register(), /* Anything we use from the VS Code proposed API */ ...proposed diff --git a/src/providers/FileSystemProvider/FileSystemProvider.ts b/src/providers/FileSystemProvider/FileSystemProvider.ts index 52c1b872..9121382b 100644 --- a/src/providers/FileSystemProvider/FileSystemProvider.ts +++ b/src/providers/FileSystemProvider/FileSystemProvider.ts @@ -347,21 +347,6 @@ export class FileSystemProvider implements vscode.FileSystemProvider { return api .getDoc(fileName, undefined, cachedFile?.mtime) .then((data) => data.result) - .then((result) => { - const fileSplit = fileName.split("."); - const fileType = fileSplit[fileSplit.length - 1]; - if (!csp && ["bpl", "dtl"].includes(fileType)) { - const partialUri = Array.isArray(result.content) ? result.content[0] : String(result.content).split("\n")[0]; - const strippedUri = partialUri.split("&STUDIO=")[0]; - const { https, host, port, pathPrefix } = api.config; - result.content = [ - `${https ? "https" : "http"}://${host}:${port}${pathPrefix}${strippedUri}`, - "Use the link above to launch the external editor in your web browser.", - "Do not edit this document here. It cannot be saved to the server.", - ]; - } - return result; - }) .then( ({ ts, content }) => new File( diff --git a/src/providers/bplDtlEditor.ts b/src/providers/bplDtlEditor.ts new file mode 100644 index 00000000..c5d4a37f --- /dev/null +++ b/src/providers/bplDtlEditor.ts @@ -0,0 +1,249 @@ +import * as vscode from "vscode"; +import { AtelierAPI } from "../api"; +import { currentFile } from "../utils/index"; +import { DocumentContentProvider } from "./DocumentContentProvider"; +import { loadChanges } from "../commands/compile"; +import { Response } from "../api/atelier"; + +// Custom text documents cannot be accessed through vscode.window.activeTextEditor +// so they must be kept track of manually for the view other command +export let currentBplDtlClassDoc: vscode.TextDocument = null; + +async function saveBplDtl(content: string[], doc: vscode.TextDocument): Promise> { + const api = new AtelierAPI(doc.uri); + const displayName = doc.fileName.slice(1); + return vscode.window.withProgress( + { + cancellable: false, + location: vscode.ProgressLocation.Notification, + title: "Compiling: " + displayName, + }, + () => + api.putDoc( + displayName, + { + enc: false, + content, + mtime: -1, + }, + true + ) + ); +} + +export class BplDtlEditorProvider implements vscode.CustomTextEditorProvider { + public static register(): vscode.Disposable { + const provider = new BplDtlEditorProvider(); + const providerRegistration = vscode.window.registerCustomEditorProvider(BplDtlEditorProvider.viewType, provider, { + webviewOptions: { retainContextWhenHidden: true }, + }); + return providerRegistration; + } + + public static readonly viewType = "vscode-objectscript.bplDtlEditor"; + + private isDirty: boolean; + + public async resolveCustomTextEditor( + document: vscode.TextDocument, + webviewPanel: vscode.WebviewPanel, + _token: vscode.CancellationToken + ): Promise { + const url = await this.getUrl(document); + if (!url) return; + + const type = document.fileName.substring(document.fileName.length - 3); + const clsName = document.fileName.substring(1, document.fileName.length - 4) + ".cls"; + const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri); + const clsUri = DocumentContentProvider.getUri(clsName, workspaceFolder?.name); + const clsDoc = await vscode.workspace.openTextDocument(clsUri); + if (!clsDoc) { + vscode.window.showErrorMessage("The class " + clsName + " could not be found."); + return; + } + const clsFile = currentFile(clsDoc); + let pageCompatible = false; + let savedInCls = false; + this.isDirty = document.isDirty; + + // Webview settings + webviewPanel.webview.html = this.getHtmlForWebview(url); + webviewPanel.webview.options = { + enableScripts: true, + }; + if (webviewPanel.active) { + currentBplDtlClassDoc = clsDoc; + } + + webviewPanel.onDidChangeViewState(async (event) => { + if (event.webviewPanel.active) { + currentBplDtlClassDoc = clsDoc; + } + }); + + // Setup webview to communicate with the iframe + webviewPanel.webview.onDidReceiveMessage(async (message) => { + if (message.confirm) { + const answer = await vscode.window.showWarningMessage(message.confirm, { modal: true }, "OK"); + webviewPanel.webview.postMessage({ direction: "toEditor", answer: answer === "OK", usePort: true }); + } else if (message.alert) { + await vscode.window.showWarningMessage(message.alert, { modal: true }); + } else if (message.modified !== undefined) { + if (message.modified === true && !this.isDirty) { + this.isDirty = true; + this.dummyEdit(document); + } else if (message.modified === false && this.isDirty) { + this.isDirty = false; + // sometimes the page reports modified true then false immediately, a timeout is required for that to succeed + setTimeout(() => vscode.commands.executeCommand("undo"), 100); + } + } else if (message.vscodeCompatible === true) { + pageCompatible = true; + } else if (message.saveError) { + vscode.window.showErrorMessage(message.saveError); + } else if (message.infoMessage) { + vscode.window.showInformationMessage(message.infoMessage); + } else if (message.xml) { + saveBplDtl([message.xml], document).then((response) => { + if (response.result.status === "") { + loadChanges([clsFile]); + } + }); + } else if (message.loaded) { + if (!pageCompatible) { + vscode.window.showErrorMessage( + `This ${type.toUpperCase()} editor is not compatible with VSCode. See (TODO) to setup VSCode compatibility.` + ); + } + } else if (message.viewOther) { + vscode.commands.executeCommand("vscode-objectscript.viewOthers"); + } + }); + + webviewPanel.onDidChangeViewState(async (e) => { + const active = e.webviewPanel.active; + if (active && savedInCls) { + let shouldReload = true; + if (this.isDirty) { + const answer = await vscode.window.showWarningMessage( + "This file has been changed, would you like to reload it?", + { modal: true }, + "Yes", + "No" + ); + shouldReload = answer === "Yes"; + } + + if (shouldReload) { + vscode.commands.executeCommand("undo"); + this.isDirty = false; + + webviewPanel.webview.postMessage({ direction: "toEditor", reload: 1 }); + savedInCls = false; + } + } + }); + + const saveDocumentSubscription = vscode.workspace.onDidSaveTextDocument((doc) => { + // send a message to the iframe to reload the editor + if (doc.uri.toString() === clsUri.toString()) { + savedInCls = true; + } else if (doc.uri.toString() === document.uri.toString()) { + console.log("telling editor to save"); + webviewPanel.webview.postMessage({ direction: "toEditor", save: 1 }); + } + }); + + webviewPanel.onDidDispose(() => saveDocumentSubscription.dispose()); + } + + private getHtmlForWebview(url: URL): string { + /* + This webview has an iframe pointing to the correct URL and manages messages between + VS Code and the iframe. + */ + return ` + + + + + + + + + + + `; + } + + private async getUrl(document: vscode.TextDocument): Promise { + // the url should be the first line of the file + const firstLine = document.getText(new vscode.Range(new vscode.Position(0, 0), new vscode.Position(1, 0))); + const strippedUri = firstLine.split("&STUDIO=")[0]; + + const api = new AtelierAPI(document.uri); + const { https, host, port, pathPrefix } = api.config; + const url = new URL(`${https ? "https" : "http"}://${host}:${port}${pathPrefix}${strippedUri}`); + + // add studio mode and a csptoken to the url + url.searchParams.set("STUDIO", "1"); + url.searchParams.set("CSPSHARE", "1"); + const response = await api.actionQuery("select %Atelier_v1_Utils.General_GetCSPToken(?) csptoken", [strippedUri]); + const csptoken = response.result.content[0].csptoken; + url.searchParams.set("CSPCHD", csptoken); + + return url; + } + + /// Make an edit to indicate unsaved changes + /// Only applies to the underlying document of the BPL/DTL file not the CLS file + private async dummyEdit(document: vscode.TextDocument) { + if (document.isDirty) return; + + const range = new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 1)); + + const insertEdit = new vscode.WorkspaceEdit(); + insertEdit.insert(document.uri, range.start, " "); + await vscode.workspace.applyEdit(insertEdit); + } +}