Skip to content

Commit 39072cd

Browse files
authored
Synchronize only the storage definition when compiling *.cls documents (#1730)
1 parent 980ffb8 commit 39072cd

File tree

7 files changed

+201
-105
lines changed

7 files changed

+201
-105
lines changed

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1430,6 +1430,11 @@
14301430
"type": "boolean",
14311431
"default": true
14321432
},
1433+
"objectscript.refreshClassesOnSync": {
1434+
"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.",
1435+
"type": "boolean",
1436+
"default": false
1437+
},
14331438
"objectscript.multilineMethodArgs": {
14341439
"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.",
14351440
"type": "boolean",

src/api/atelier.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export interface Document {
4141
cat: "RTN" | "CLS" | "CSP" | "OTH";
4242
status: string;
4343
enc: boolean;
44-
flags: number;
44+
flags: 0 | 1;
4545
content: string[] | Buffer;
4646
ext?: UserAction | UserAction[];
4747
}

src/api/index.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -620,7 +620,12 @@ export class AtelierAPI {
620620
}
621621

622622
// api v1+
623-
public getDoc(name: string, scope: vscode.Uri | string, mtime?: number): Promise<Atelier.Response<Atelier.Document>> {
623+
public getDoc(
624+
name: string,
625+
scope: vscode.Uri | string,
626+
mtime?: number,
627+
storageOnly: boolean = false
628+
): Promise<Atelier.Response<Atelier.Document>> {
624629
let params, headers;
625630
name = this.transformNameIfCsp(name);
626631
if (
@@ -636,6 +641,11 @@ export class AtelierAPI {
636641
.get("multilineMethodArgs")
637642
) {
638643
params = { format: "udl-multiline" };
644+
} else {
645+
params = {};
646+
}
647+
if (storageOnly) {
648+
params["storageOnly"] = "1";
639649
}
640650
if (mtime && mtime > 0) {
641651
headers = { "IF-NONE-MATCH": new Date(mtime).toISOString().replace(/T|Z/g, " ").trim() };
@@ -658,7 +668,7 @@ export class AtelierAPI {
658668
name: string,
659669
data: { enc: boolean; content: string[]; mtime: number },
660670
ignoreConflict?: boolean
661-
): Promise<Atelier.Response> {
671+
): Promise<Atelier.Response<Atelier.Document>> {
662672
const params = { ignoreConflict };
663673
name = this.transformNameIfCsp(name);
664674
const headers = {};

src/commands/compile.ts

Lines changed: 162 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
exportedUris,
2525
getWsFolder,
2626
handleError,
27+
isClass,
2728
isClassDeployed,
2829
isClassOrRtn,
2930
isCompilable,
@@ -37,6 +38,7 @@ import {
3738
import { StudioActions } from "./studio";
3839
import { NodeBase, PackageNode, RootNode } from "../explorer/nodes";
3940
import { getUrisForDocument, updateIndex } from "../utils/documentIndex";
41+
import { Document } from "../api/atelier";
4042

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

85+
// Synchronize the client version and the server version of the same file
8386
export async function importFile(
8487
file: CurrentTextFile | CurrentBinaryFile,
88+
willCompile: boolean,
8589
ignoreConflict?: boolean,
8690
skipDeplCheck = false
8791
): Promise<any> {
@@ -114,78 +118,88 @@ export async function importFile(
114118
mtime < 0 ||
115119
(notIsfs(file.uri) &&
116120
vscode.workspace.getConfiguration("objectscript", file.uri).get<boolean>("overwriteServerChanges"));
117-
return api
118-
.putDoc(
121+
try {
122+
const data = await api.putDoc(
119123
file.name,
120124
{
121125
content,
122126
enc,
123127
mtime,
124128
},
125129
ignoreConflict
126-
)
127-
.then((data) => {
128-
// Update cache entry
129-
workspaceState.update(`${file.uniqueId}:mtime`, Number(new Date(data.result.ts + "Z")));
130+
);
131+
// Update cache entry
132+
workspaceState.update(`${file.uniqueId}:mtime`, Number(new Date(data.result.ts + "Z")));
130133

131-
// In case another extension has used an 'objectscript://' uri to load a document read-only from the server,
132-
// make it reload with what we just imported to the server.
133-
const serverUri = DocumentContentProvider.getUri(
134-
file.name,
135-
file.workspaceFolder,
136-
undefined,
137-
false,
138-
undefined,
139-
true
140-
);
141-
if (serverUri) documentContentProvider.update(serverUri);
142-
})
143-
.catch((error) => {
144-
if (error?.statusCode == 409) {
145-
const choices: string[] = [];
146-
if (!enc) {
147-
choices.push("Compare");
148-
}
149-
choices.push("Overwrite on Server", "Pull Server Changes", "Cancel");
150-
return vscode.window
151-
.showErrorMessage(
152-
`Failed to import '${file.name}': The version of the file on the server is newer.
134+
if (!willCompile && isClass(file.name) && data.result.content.length) {
135+
// In this case, the file must be a CLS and data.result.content must be the new Storage definitions
136+
// (with the rest of the class if flags === 0)
137+
const oldContent = new TextDecoder().decode(await vscode.workspace.fs.readFile(file.uri));
138+
const oldContentArray = oldContent.split(/\r?\n/);
139+
const storage = Buffer.isBuffer(data.result.content)
140+
? new TextDecoder().decode(data.result.content).split(/\r?\n/)
141+
: data.result.content;
142+
const newContentArray = updateStorage(oldContentArray, storage);
143+
if (oldContentArray.some((oldLine, index) => oldLine !== newContentArray[index])) {
144+
const EOL = ((<CurrentTextFile>file)?.eol ?? vscode.EndOfLine.LF) == vscode.EndOfLine.CRLF ? "\r\n" : "\n";
145+
const newContent = newContentArray.join(EOL);
146+
await vscode.workspace.fs.writeFile(file.uri, new TextEncoder().encode(newContent));
147+
}
148+
}
149+
// In case another extension has used an 'objectscript://' uri to load a document read-only from the server,
150+
// make it reload with what we just imported to the server.
151+
const serverUri = DocumentContentProvider.getUri(
152+
file.name,
153+
file.workspaceFolder,
154+
undefined,
155+
false,
156+
undefined,
157+
true
158+
);
159+
if (serverUri) documentContentProvider.update(serverUri);
160+
} catch (error) {
161+
if (error?.statusCode == 409) {
162+
const choices: string[] = [];
163+
if (!enc) {
164+
choices.push("Compare");
165+
}
166+
choices.push("Overwrite on Server", "Pull Server Changes", "Cancel");
167+
const action = await vscode.window.showErrorMessage(
168+
`Failed to import '${file.name}': The version of the file on the server is newer.
153169
What do you want to do?`,
154-
...choices
155-
)
156-
.then((action) => {
157-
switch (action) {
158-
case "Compare":
159-
return vscode.commands
160-
.executeCommand(
161-
"vscode.diff",
162-
vscode.Uri.file(file.name).with({
163-
scheme: OBJECTSCRIPT_FILE_SCHEMA,
164-
authority: file.workspaceFolder,
165-
query: file.name.includes("/") ? "csp" : "",
166-
}),
167-
file.uri,
168-
`Server • ${file.name} ↔ Local • ${file.fileName}`
169-
)
170-
.then(() => Promise.reject());
171-
case "Overwrite on Server":
172-
// Clear cache entry
173-
workspaceState.update(`${file.uniqueId}:mtime`, undefined);
174-
// Overwrite
175-
return importFile(file, true, true);
176-
case "Pull Server Changes":
177-
loadChanges([file]);
178-
return Promise.reject();
179-
case "Cancel":
180-
return Promise.reject();
181-
}
182-
return Promise.reject();
183-
});
184-
} else {
185-
handleError(error, `Failed to save file '${file.name}' on the server.`);
186-
return Promise.reject();
170+
...choices
171+
);
172+
switch (action) {
173+
case "Compare":
174+
return vscode.commands
175+
.executeCommand(
176+
"vscode.diff",
177+
vscode.Uri.file(file.name).with({
178+
scheme: OBJECTSCRIPT_FILE_SCHEMA,
179+
authority: file.workspaceFolder,
180+
query: file.name.includes("/") ? "csp" : "",
181+
}),
182+
file.uri,
183+
`Server • ${file.name} ↔ Local • ${file.fileName}`
184+
)
185+
.then(() => Promise.reject());
186+
case "Overwrite on Server":
187+
// Clear cache entry
188+
workspaceState.update(`${file.uniqueId}:mtime`, undefined);
189+
// Overwrite
190+
return importFile(file, willCompile, true, true);
191+
case "Pull Server Changes":
192+
loadChanges([file]);
193+
return Promise.reject();
194+
case "Cancel":
195+
return Promise.reject();
187196
}
188-
});
197+
return Promise.reject();
198+
} else {
199+
handleError(error, `Failed to save file '${file.name}' on the server.`);
200+
return Promise.reject();
201+
}
202+
}
189203
}
190204

191205
function updateOthers(others: string[], baseUri: vscode.Uri) {
@@ -218,7 +232,21 @@ export async function loadChanges(files: (CurrentTextFile | CurrentBinaryFile)[]
218232
const mtime = Number(new Date(doc.ts + "Z"));
219233
workspaceState.update(`${file.uniqueId}:mtime`, mtime > 0 ? mtime : undefined);
220234
if (notIsfs(file.uri)) {
221-
const content = await api.getDoc(file.name, file.uri).then((data) => data.result.content);
235+
let content: Document["content"];
236+
if (
237+
!(
238+
isClass(file.uri.path) &&
239+
!vscode.workspace.getConfiguration("objectscript", file.uri).get("refreshClassesOnSync")
240+
)
241+
) {
242+
content = (await api.getDoc(file.name, file.uri)).result.content;
243+
} else {
244+
// Insert/update the storage part of class definition.
245+
content = new TextDecoder().decode(await vscode.workspace.fs.readFile(file.uri)).split(/\r?\n/);
246+
let storage = (await api.getDoc(file.name, file.uri, undefined, true)).result.content;
247+
storage = Buffer.isBuffer(storage) ? new TextDecoder().decode(storage).split(/\r?\n/) : storage;
248+
content = updateStorage(content, storage);
249+
}
222250
exportedUris.add(file.uri.toString()); // Set optimistically
223251
await vscode.workspace.fs
224252
.writeFile(
@@ -250,6 +278,52 @@ export async function loadChanges(files: (CurrentTextFile | CurrentBinaryFile)[]
250278
);
251279
}
252280

281+
function updateStorage(content: string[], storage: string[]): string[] {
282+
const storageMap = storageToMap(storage);
283+
let contentString = content.join("\n");
284+
contentString = contentString
285+
// update existing Storages
286+
.replaceAll(/\n(\s*storage\s+(\w+)\s*{\s*)([^}]*?)(\s*})/gim, (_match, beforeXML, name, _oldXML, afterXML) => {
287+
const newXML = storageMap.get(name);
288+
if (newXML === undefined) {
289+
return "";
290+
}
291+
storageMap.delete(name);
292+
return "\n" + beforeXML + newXML + afterXML;
293+
});
294+
contentString = contentString
295+
// insert remaining Storages
296+
.replace(/}\s*$/, (m) => {
297+
for (const [name, content] of storageMap.entries()) {
298+
m = `Storage ${name}\n{\n${content}\n}\n\n${m}`;
299+
}
300+
return m;
301+
});
302+
return contentString.split("\n");
303+
}
304+
305+
function storageToMap(storage: string[]): Map<string, string> {
306+
const map: Map<string, string> = new Map();
307+
let k: string;
308+
let v = [];
309+
for (const line of storage) {
310+
if (line.startsWith("Storage ")) {
311+
k = line.slice("Storage ".length, line.length);
312+
v = [];
313+
} else if (k !== undefined) {
314+
if (line === "{") {
315+
continue;
316+
} else if (line === "}") {
317+
map.set(k, v.join("\n"));
318+
k = undefined;
319+
} else {
320+
v.push(line);
321+
}
322+
}
323+
}
324+
return map;
325+
}
326+
253327
export async function compile(docs: (CurrentTextFile | CurrentBinaryFile)[], askFlags = false): Promise<any> {
254328
docs = docs.filter(notNull);
255329
if (!docs.length) return;
@@ -318,14 +392,16 @@ export async function importAndCompile(document?: vscode.TextDocument, askFlags
318392
return;
319393
}
320394

321-
return (
322-
importFile(file)
323-
.then(() => {
324-
if (isCompilable(file.name)) compile([file], askFlags);
325-
})
326-
// importFile handles any server errors
327-
.catch(() => {})
328-
);
395+
try {
396+
if (isCompilable(file.name)) {
397+
await importFile(file, true);
398+
compile([file], askFlags);
399+
} else {
400+
await importFile(file, false);
401+
}
402+
} catch {
403+
// importFile handles any server errors
404+
}
329405
}
330406

331407
export async function compileOnly(document?: vscode.TextDocument, askFlags = false): Promise<any> {
@@ -395,28 +471,26 @@ async function importFiles(files: vscode.Uri[], noCompile = false) {
395471
await Promise.allSettled<void>(
396472
files.map((uri) =>
397473
rateLimiter.call(async () => {
398-
return vscode.workspace.fs
399-
.readFile(uri)
400-
.then((contentBytes) =>
401-
currentFileFromContent(
402-
uri,
403-
isText(uri.path.split("/").pop(), Buffer.from(contentBytes))
404-
? new TextDecoder().decode(contentBytes)
405-
: Buffer.from(contentBytes)
406-
)
407-
)
408-
.then((curFile) => {
409-
if (curFile) {
410-
if (typeof curFile.content == "string" && isCompilable(curFile.name)) toCompile.push(curFile);
411-
return importFile(curFile).then(() => outputChannel.appendLine("Imported file: " + curFile.fileName));
412-
}
413-
});
474+
const contentBytes = await vscode.workspace.fs.readFile(uri);
475+
const curFile = currentFileFromContent(
476+
uri,
477+
isText(uri.path.split("/").pop(), Buffer.from(contentBytes))
478+
? new TextDecoder().decode(contentBytes)
479+
: Buffer.from(contentBytes)
480+
);
481+
if (curFile) {
482+
if (typeof curFile.content == "string" && isCompilable(curFile.name)) {
483+
toCompile.push(curFile);
484+
}
485+
await importFile(curFile, !noCompile);
486+
outputChannel.appendLine("Imported file: " + curFile.fileName);
487+
}
414488
})
415489
)
416490
);
417491

418492
if (!noCompile && toCompile.length > 0) {
419-
return compile(toCompile);
493+
await compile(toCompile);
420494
}
421495
return;
422496
}

src/extension.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -806,6 +806,7 @@ function sendWsFolderTelemetryEvent(wsFolders: readonly vscode.WorkspaceFolder[]
806806
"config.syncLocalChanges": !serverSide ? conf.get("syncLocalChanges") : undefined,
807807
dockerCompose: !serverSide ? String(typeof conf.get("conn.docker-compose") == "object") : undefined,
808808
"config.conn.links": String(Object.keys(conf.get("conn.links", {})).length),
809+
"config.refreshClassesOnSync": !serverSide ? conf.get("refreshClassesOnSync") : undefined,
809810
});
810811
});
811812
}

0 commit comments

Comments
 (0)