Skip to content
Open
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
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1416,6 +1416,11 @@
"type": "boolean",
"default": true
},
"objectscript.refreshClassesOnSync": {
"description": "Controls whether the entire content of client-side classes is replaced with the server copy after synchronizing with the server. If `false`, only the contents of Storage definitions are replaced.",
"type": "boolean",
"default": false
},
"objectscript.multilineMethodArgs": {
"markdownDescription": "List method arguments on multiple lines, if the server supports it. **NOTE:** Only supported on IRIS 2019.1.2, 2020.1.1+, 2021.1.0+ and subsequent versions! On all other versions, this setting will have no effect.",
"type": "boolean",
Expand Down
2 changes: 1 addition & 1 deletion src/api/atelier.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export interface Document {
cat: "RTN" | "CLS" | "CSP" | "OTH";
status: string;
enc: boolean;
flags: number;
flags: 0 | 1;
content: string[] | Buffer;
ext?: UserAction | UserAction[];
}
Expand Down
14 changes: 12 additions & 2 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -626,7 +626,12 @@ export class AtelierAPI {
}

// api v1+
public getDoc(name: string, scope: vscode.Uri | string, mtime?: number): Promise<Atelier.Response<Atelier.Document>> {
public getDoc(
name: string,
scope: vscode.Uri | string,
mtime?: number,
storageOnly: boolean = false
): Promise<Atelier.Response<Atelier.Document>> {
let params, headers;
name = this.transformNameIfCsp(name);
if (
Expand All @@ -642,6 +647,11 @@ export class AtelierAPI {
.get("multilineMethodArgs")
) {
params = { format: "udl-multiline" };
} else {
params = {};
}
if (storageOnly) {
params["storageOnly"] = "1";
}
if (mtime && mtime > 0) {
headers = { "IF-NONE-MATCH": new Date(mtime).toISOString().replace(/T|Z/g, " ").trim() };
Expand All @@ -664,7 +674,7 @@ export class AtelierAPI {
name: string,
data: { enc: boolean; content: string[]; mtime: number },
ignoreConflict?: boolean
): Promise<Atelier.Response> {
): Promise<Atelier.Response<Atelier.Document>> {
const params = { ignoreConflict };
name = this.transformNameIfCsp(name);
const headers = {};
Expand Down
250 changes: 161 additions & 89 deletions src/commands/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
exportedUris,
getWsFolder,
handleError,
isClass,
isClassDeployed,
isClassOrRtn,
isCompilable,
Expand All @@ -37,6 +38,7 @@ import {
import { StudioActions } from "./studio";
import { NodeBase, PackageNode, RootNode } from "../explorer/nodes";
import { getUrisForDocument, updateIndex } from "../utils/documentIndex";
import { Document } from "../api/atelier";

/**
* For files being locally edited, get and return its mtime timestamp from workspace-state cache if present there,
Expand Down Expand Up @@ -80,8 +82,10 @@ export async function checkChangedOnServer(file: CurrentTextFile | CurrentBinary
return mtime;
}

// Synchronize the client version and the server version of the same file
export async function importFile(
file: CurrentTextFile | CurrentBinaryFile,
willCompile: boolean,
ignoreConflict?: boolean,
skipDeplCheck = false
): Promise<any> {
Expand Down Expand Up @@ -114,78 +118,86 @@ export async function importFile(
mtime < 0 ||
(notIsfs(file.uri) &&
vscode.workspace.getConfiguration("objectscript", file.uri).get<boolean>("overwriteServerChanges"));
return api
.putDoc(
try {
const data = await api.putDoc(
file.name,
{
content,
enc,
mtime,
},
ignoreConflict
)
.then((data) => {
// Update cache entry
workspaceState.update(`${file.uniqueId}:mtime`, Number(new Date(data.result.ts + "Z")));

// In case another extension has used an 'objectscript://' uri to load a document read-only from the server,
// make it reload with what we just imported to the server.
const serverUri = DocumentContentProvider.getUri(
file.name,
file.workspaceFolder,
undefined,
false,
undefined,
true
);
documentContentProvider.update(serverUri.with({ scheme: OBJECTSCRIPT_FILE_SCHEMA }));
})
.catch((error) => {
if (error?.statusCode == 409) {
const choices: string[] = [];
if (!enc) {
choices.push("Compare");
}
choices.push("Overwrite on Server", "Pull Server Changes", "Cancel");
return vscode.window
.showErrorMessage(
`Failed to import '${file.name}': The version of the file on the server is newer.
);
workspaceState.update(`${file.uniqueId}:mtime`, Number(new Date(data.result.ts + "Z")));
if (!willCompile && isClass(file.name) && data.result.content.length) {
// In this case, the file must be a CLS and data.result.content must be the new Storage definitions
// (with the rest of the class if flags === 0)
const oldContent = new TextDecoder().decode(await vscode.workspace.fs.readFile(file.uri));
const oldContentArray = oldContent.split(/\r?\n/);
const storage = Buffer.isBuffer(data.result.content)
? new TextDecoder().decode(data.result.content).split(/\r?\n/)
: data.result.content;
const newContentArray = updateStorage(oldContentArray, storage);
if (oldContentArray.some((oldLine, index) => oldLine !== newContentArray[index])) {
const EOL = ((<CurrentTextFile>file)?.eol ?? vscode.EndOfLine.LF) == vscode.EndOfLine.CRLF ? "\r\n" : "\n";
const newContent = newContentArray.join(EOL);
await vscode.workspace.fs.writeFile(file.uri, new TextEncoder().encode(newContent));
}
}
// In case another extension has used an 'objectscript://' uri to load a document read-only from the server,
// make it reload with what we just imported to the server.
const serverUri = DocumentContentProvider.getUri(
file.name,
file.workspaceFolder,
undefined,
false,
undefined,
true
);
documentContentProvider.update(serverUri.with({ scheme: OBJECTSCRIPT_FILE_SCHEMA }));
return;
} catch (error) {
if (error?.statusCode == 409) {
const choices: string[] = [];
if (!enc) {
choices.push("Compare");
}
choices.push("Overwrite on Server", "Pull Server Changes", "Cancel");
const action = await vscode.window.showErrorMessage(
`Failed to import '${file.name}': The version of the file on the server is newer.
What do you want to do?`,
...choices
)
.then((action) => {
switch (action) {
case "Compare":
return vscode.commands
.executeCommand(
"vscode.diff",
vscode.Uri.file(file.name).with({
scheme: OBJECTSCRIPT_FILE_SCHEMA,
authority: file.workspaceFolder,
query: file.name.includes("/") ? "csp" : "",
}),
file.uri,
`Server • ${file.name} ↔ Local • ${file.fileName}`
)
.then(() => Promise.reject());
case "Overwrite on Server":
// Clear cache entry
workspaceState.update(`${file.uniqueId}:mtime`, undefined);
// Overwrite
return importFile(file, true, true);
case "Pull Server Changes":
loadChanges([file]);
return Promise.reject();
case "Cancel":
return Promise.reject();
}
return Promise.reject();
});
} else {
handleError(error, `Failed to save file '${file.name}' on the server.`);
return Promise.reject();
...choices
);
switch (action) {
case "Compare":
await vscode.commands.executeCommand(
"vscode.diff",
vscode.Uri.file(file.name).with({
scheme: OBJECTSCRIPT_FILE_SCHEMA,
authority: file.workspaceFolder,
query: file.name.includes("/") ? "csp" : "",
}),
file.uri,
`Server • ${file.name} ↔ Local • ${file.fileName}`
);
return Promise.reject();
case "Overwrite on Server":
// Clear cache entry
workspaceState.update(`${file.uniqueId}:mtime`, undefined);
// Overwrite
return importFile(file, willCompile, true, true);
case "Pull Server Changes":
loadChanges([file]);
return Promise.reject();
case "Cancel":
return Promise.reject();
}
});
return Promise.reject();
} else {
handleError(error, `Failed to save file '${file.name}' on the server.`);
return Promise.reject();
}
}
}

function updateOthers(others: string[], baseUri: vscode.Uri) {
Expand Down Expand Up @@ -218,7 +230,21 @@ export async function loadChanges(files: (CurrentTextFile | CurrentBinaryFile)[]
const mtime = Number(new Date(doc.ts + "Z"));
workspaceState.update(`${file.uniqueId}:mtime`, mtime > 0 ? mtime : undefined);
if (notIsfs(file.uri)) {
const content = await api.getDoc(file.name, file.uri).then((data) => data.result.content);
let content: Document["content"];
if (
!(
isClass(file.uri.path) &&
!vscode.workspace.getConfiguration("objectscript", file.uri).get("refreshClassesOnSync")
)
) {
content = (await api.getDoc(file.name, file.uri)).result.content;
} else {
// Insert/update the storage part of class definition.
content = new TextDecoder().decode(await vscode.workspace.fs.readFile(file.uri)).split(/\r?\n/);
let storage = (await api.getDoc(file.name, file.uri, undefined, true)).result.content;
storage = Buffer.isBuffer(storage) ? new TextDecoder().decode(storage).split(/\r?\n/) : storage;
content = updateStorage(content, storage);
}
exportedUris.add(file.uri.toString()); // Set optimistically
await vscode.workspace.fs
.writeFile(
Expand Down Expand Up @@ -250,6 +276,52 @@ export async function loadChanges(files: (CurrentTextFile | CurrentBinaryFile)[]
);
}

function updateStorage(content: string[], storage: string[]): string[] {
const storageMap = storageToMap(storage);
let contentString = content.join("\n");
contentString = contentString
// update existing Storages
.replaceAll(/\n(\s*storage\s+(\w+)\s*{\s*)([^}]*?)(\s*})/gim, (_match, beforeXML, name, _oldXML, afterXML) => {
const newXML = storageMap.get(name);
if (newXML === undefined) {
return "";
}
storageMap.delete(name);
return "\n" + beforeXML + newXML + afterXML;
});
contentString = contentString
// insert remaining Storages
.replace(/}\s*$/, (m) => {
for (const [name, content] of storageMap.entries()) {
m = `Storage ${name}\n{\n${content}\n}\n\n${m}`;
}
return m;
});
return contentString.split("\n");
}

function storageToMap(storage: string[]): Map<string, string> {
const map: Map<string, string> = new Map();
let k: string;
let v = [];
for (const line of storage) {
if (line.startsWith("Storage ")) {
k = line.slice("Storage ".length, line.length);
v = [];
} else if (k !== undefined) {
if (line === "{") {
continue;
} else if (line === "}") {
map.set(k, v.join("\n"));
k = undefined;
} else {
v.push(line);
}
}
}
return map;
}

export async function compile(docs: (CurrentTextFile | CurrentBinaryFile)[], askFlags = false): Promise<any> {
docs = docs.filter(notNull);
if (!docs.length) return;
Expand Down Expand Up @@ -318,14 +390,16 @@ export async function importAndCompile(document?: vscode.TextDocument, askFlags
return;
}

return (
importFile(file)
.then(() => {
if (isCompilable(file.name)) compile([file], askFlags);
})
// importFile handles any server errors
.catch(() => {})
);
try {
if (isCompilable(file.name)) {
await importFile(file, true);
compile([file], askFlags);
} else {
await importFile(file, false);
}
} catch {
// importFile handles any server errors
}
}

export async function compileOnly(document?: vscode.TextDocument, askFlags = false): Promise<any> {
Expand Down Expand Up @@ -395,28 +469,26 @@ async function importFiles(files: vscode.Uri[], noCompile = false) {
await Promise.allSettled<void>(
files.map((uri) =>
rateLimiter.call(async () => {
return vscode.workspace.fs
.readFile(uri)
.then((contentBytes) =>
currentFileFromContent(
uri,
isText(uri.path.split("/").pop(), Buffer.from(contentBytes))
? new TextDecoder().decode(contentBytes)
: Buffer.from(contentBytes)
)
)
.then((curFile) => {
if (curFile) {
if (typeof curFile.content == "string" && isCompilable(curFile.name)) toCompile.push(curFile);
return importFile(curFile).then(() => outputChannel.appendLine("Imported file: " + curFile.fileName));
}
});
const contentBytes = await vscode.workspace.fs.readFile(uri);
const curFile = currentFileFromContent(
uri,
isText(uri.path.split("/").pop(), Buffer.from(contentBytes))
? new TextDecoder().decode(contentBytes)
: Buffer.from(contentBytes)
);
if (curFile) {
if (typeof curFile.content == "string" && isCompilable(curFile.name)) {
toCompile.push(curFile);
}
await importFile(curFile, !noCompile);
outputChannel.appendLine("Imported file: " + curFile.fileName);
}
})
)
);

if (!noCompile && toCompile.length > 0) {
return compile(toCompile);
await compile(toCompile);
}
return;
}
Expand Down
1 change: 1 addition & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,7 @@ function sendWsFolderTelemetryEvent(wsFolders: readonly vscode.WorkspaceFolder[]
"config.syncLocalChanges": !serverSide ? conf.get("syncLocalChanges") : undefined,
dockerCompose: !serverSide ? String(typeof conf.get("conn.docker-compose") == "object") : undefined,
"config.conn.links": String(Object.keys(conf.get("conn.links", {})).length),
"config.refreshClassesOnSync": !serverSide ? conf.get("refreshClassesOnSync") : undefined,
});
});
}
Expand Down
Loading