Skip to content
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

Add GUI editors for BPL and DTL files #730

Closed
wants to merge 3 commits into from
Closed
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
17 changes: 16 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
}
],
"engines": {
"vscode": "^1.43.0"
"vscode": "^1.59.0"
},
"enableProposedApi": true,
"activationEvents": [
Expand Down Expand Up @@ -1059,6 +1059,21 @@
]
}
],
"customEditors": [
{
"viewType": "vscode-objectscript.bplDtlEditor",
"displayName": "BPL Editor",
"selector": [
{
"filenamePattern": "*.bpl"
},
{
"filenamePattern": "*.dtl"
}
],
"priority": "default"
}
],
"resourceLabelFormatters": [
{
"scheme": "isfs",
Expand Down
18 changes: 16 additions & 2 deletions src/commands/viewOthers.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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) {
Expand Down Expand Up @@ -77,15 +83,23 @@ export async function viewOthers(forceEditable = false): Promise<void> {
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) {
uri = DocumentContentProvider.getUri(item, undefined, undefined, forceEditable);
} 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);
}
}
};

Expand Down
5 changes: 4 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -578,7 +579,8 @@ export async function activate(context: vscode.ExtensionContext): Promise<any> {

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") {
Expand Down Expand Up @@ -921,6 +923,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<any> {
new DocumentLinkProvider()
),
vscode.commands.registerCommand("vscode-objectscript.editOthers", () => viewOthers(true)),
BplDtlEditorProvider.register(),

/* Anything we use from the VS Code proposed API */
...proposed
Expand Down
15 changes: 0 additions & 15 deletions src/providers/FileSystemProvider/FileSystemProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
249 changes: 249 additions & 0 deletions src/providers/bplDtlEditor.ts
Original file line number Diff line number Diff line change
@@ -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<Response<any>> {
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<void> {
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 `
<!DOCTYPE html>
<html lang="en">
<head>
<style type="text/css">
body, html {
margin: 0; padding: 0; height: 100%; overflow: hidden;
background-color: white;
}
#content {
position:absolute; left: 0; right: 0; bottom: 0; top: 0px;
}
</style>
</head>
<body>
<iframe src="${url.toString()}" id="editor" width="100%" height="100%" frameborder="0"></iframe>
<script>
(function() {
const vscode = acquireVsCodeApi();

// after loading send a message to check for compatibility
window.onload = (event) => {
vscode.postMessage({loaded: true});
}

// message passing, this code is in between vscode and the zen page, must pass to both
var port;
window.onmessage = (event) => {
const data = event.data;
const iframe = document.getElementById('editor').contentWindow;

if (data.direction === "toEditor") {
if (data.usePort === true) {
port.postMessage(event.data);
port = null;
} else {
iframe.postMessage(data, '*');
}
}

else if (data.direction === "toVSCode") {
vscode.postMessage(data);
if (data.usePort === true) {
port = event.ports[0];
}
}
}
}())
</script>
</body>
</html>
`;
}

private async getUrl(document: vscode.TextDocument): Promise<URL> {
// 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);
}
}