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
37 changes: 36 additions & 1 deletion packages/file-scheme/__tests__/browser/resource.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { IApplicationService, IJSONSchemaRegistry, ISchemaStore, localize } from '@opensumi/ide-core-browser';
import { DefaultUriLabelProvider } from '@opensumi/ide-core-browser/lib/services';
import { CommonServerPath } from '@opensumi/ide-core-common';
import { BinaryBuffer, Disposable, OperatingSystem, URI } from '@opensumi/ide-core-common';
import {
BinaryBuffer,
Disposable,
OperatingSystem,
SaveTaskErrorCause,
SaveTaskResponseState,
URI,
} from '@opensumi/ide-core-common';
import {
HashCalculateServiceImpl,
IHashCalculateService,
Expand Down Expand Up @@ -182,4 +189,32 @@ describe('file scheme tests', () => {
}),
);
});

it('should fallback to saveByContent when saveByChange returns USE_BY_CONTENT', async () => {
const documentProvider = injector.get(FileSchemeDocumentProvider);

const saveByContent = jest.fn(() => ({ state: SaveTaskResponseState.SUCCESS }));
injector.mock(FileSchemeDocNodeServicePath, '$saveByContent', saveByContent);

const saveByChange = jest.fn(() => ({
state: SaveTaskResponseState.ERROR,
errorMessage: SaveTaskErrorCause.USE_BY_CONTENT,
}));
injector.mock(FileSchemeDocNodeServicePath, '$saveByChange', saveByChange);

// Content exceeding FILE_SAVE_BY_CHANGE_THRESHOLD (100KB) to trigger saveByChange path
const largeContent = 'x'.repeat(100001);

const result = await documentProvider.saveDocumentModel(
new URI('file:///test-fallback.ts'),
largeContent,
'baseContent',
[],
'utf8',
);

expect(saveByChange).toHaveBeenCalledTimes(1);
expect(saveByContent).toHaveBeenCalledTimes(1);
expect(result.state).toBe(SaveTaskResponseState.SUCCESS);
});
});
46 changes: 40 additions & 6 deletions packages/file-scheme/__tests__/node/file-doc-node.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { statSync } from 'fs';
import { tmpdir } from 'os';
import { dirname, join } from 'path';

Expand Down Expand Up @@ -48,10 +49,17 @@ describe('node file doc service test', () => {
if (uriString.indexOf('notexist') > -1) {
return undefined;
}
const fsPath = new URI(uriString).codeUri.fsPath;
let lastModification = Date.now();
try {
lastModification = statSync(fsPath).mtime.getTime();
} catch {
// virtual file, use Date.now()
}
const stat: FileStat = {
uri: uriString,
isDirectory: false,
lastModification: Date.now(),
lastModification,
};
return stat;
}),
Expand All @@ -60,10 +68,20 @@ describe('node file doc service test', () => {
injector.mock(
IFileService,
'resolveContent',
jest.fn((stat: FileStat) => ({
stat,
content: 'current content',
})),
jest.fn(async (uriString: string, options?: { encoding?: string }) => {
const fsPath = new URI(uriString).codeUri.fsPath;
let content: string;
try {
const buf = await readFile(fsPath);
content =
options?.encoding && options.encoding !== 'utf8' && options.encoding !== 'utf-8'
? iconvDecode(buf, options.encoding)
: buf.toString('utf8');
} catch {
content = 'current content';
}
return { stat: { uri: uriString, isDirectory: false, lastModification: Date.now() }, content };
}),
);

injector.mock(
Expand All @@ -73,7 +91,23 @@ describe('node file doc service test', () => {
);

injector.mock(IFileService, 'createFile', jest.fn());
injector.mock(IFileService, 'setContent', jest.fn());
injector.mock(
IFileService,
'setContent',
jest.fn(async (stat: FileStat, content: string, options?: { encoding?: string }) => {
const fsPath = new URI(stat.uri).codeUri.fsPath;
try {
const buf =
options?.encoding && options.encoding !== 'utf8' && options.encoding !== 'utf-8'
? iconvEncode(content, options.encoding)
: Buffer.from(content, 'utf8');
await writeFile(fsPath, buf);
} catch {
// virtual file, no-op
}
return stat;
}),
);

const fileDocNodeService: IFileSchemeDocNodeService = injector.get(FileSchemeDocNodeServiceImpl);
const fileService: IFileService = injector.get(IFileService);
Expand Down
17 changes: 16 additions & 1 deletion packages/file-scheme/src/browser/file-doc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
ISchemaStore,
MaybePromise,
PreferenceService,
SaveTaskErrorCause,
SaveTaskResponseState,
Schemes,
URI,
} from '@opensumi/ide-core-browser';
Expand Down Expand Up @@ -70,7 +72,7 @@ export class FileSchemeDocumentProvider
): Promise<IEditorDocumentModelSaveResult> {
const baseMd5 = this.hashCalculateService.calculate(baseContent);
if (content.length > FILE_SAVE_BY_CHANGE_THRESHOLD) {
return await this.fileSchemeDocClient.saveByChange(
const result = await this.fileSchemeDocClient.saveByChange(
uri.toString(),
{
baseMd5,
Expand All @@ -81,6 +83,19 @@ export class FileSchemeDocumentProvider
ignoreDiff,
token,
);
if (result.state === SaveTaskResponseState.ERROR && result.errorMessage === SaveTaskErrorCause.USE_BY_CONTENT) {
return await this.fileSchemeDocClient.saveByContent(
uri.toString(),
{
baseMd5,
content,
},
encoding,
ignoreDiff,
token,
);
}
return result;
} else {
return await this.fileSchemeDocClient.saveByContent(
uri.toString(),
Expand Down
61 changes: 27 additions & 34 deletions packages/file-scheme/src/node/file-scheme-doc.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { existsSync, readFile, statSync, writeFile } from 'fs-extra';

import { Autowired, Injectable } from '@opensumi/di';
import { IHashCalculateService } from '@opensumi/ide-core-common/lib/hash-calculate/hash-calculate';
import {
Expand All @@ -10,9 +8,6 @@ import {
SaveTaskErrorCause,
SaveTaskResponseState,
Throttler,
URI,
iconvDecode,
iconvEncode,
isEditChange,
} from '@opensumi/ide-core-node';
import { IFileService } from '@opensumi/ide-file-service';
Expand All @@ -29,8 +24,6 @@ export class FileSchemeDocNodeServiceImpl implements IFileSchemeDocNodeService {

private saveQueueByUri = new Map<string, Throttler>();

// 由于此处只处理file协议,为了简洁,不再使用 fileService,

async $saveByChange(
uri: string,
change: IContentChange,
Expand All @@ -39,39 +32,39 @@ export class FileSchemeDocNodeServiceImpl implements IFileSchemeDocNodeService {
token?: CancellationToken,
): Promise<IEditorDocumentModelSaveResult> {
try {
const fsPath = new URI(uri).codeUri.fsPath;
if (existsSync(fsPath)) {
const mtime = statSync(fsPath).mtime.getTime();
const contentBuffer = await readFile(fsPath);
if (token?.isCancellationRequested) {
return {
state: SaveTaskResponseState.ERROR,
errorMessage: SaveTaskErrorCause.CANCEL,
};
}
const content = iconvDecode(contentBuffer, encoding ? encoding : 'utf8');
if (!force) {
const currentMd5 = this.hashCalculateService.calculate(content);
if (change.baseMd5 !== currentMd5) {
return {
state: SaveTaskResponseState.DIFF,
};
}
}
const contentRes = applyChanges(content, change.changes!, change.eol);
if (statSync(fsPath).mtime.getTime() !== mtime) {
throw new Error('File has been modified during saving, please retry');
}
await writeFile(fsPath, iconvEncode(contentRes, encoding ? encoding : 'utf8'));
const stat = await this.fileService.getFileStat(uri);
if (!stat) {
return {
state: SaveTaskResponseState.SUCCESS,
state: SaveTaskResponseState.ERROR,
errorMessage: SaveTaskErrorCause.USE_BY_CONTENT,
};
} else {
}
const mtime = stat.lastModification;
const res = await this.fileService.resolveContent(uri, { encoding });
if (token?.isCancellationRequested) {
return {
state: SaveTaskResponseState.ERROR,
errorMessage: SaveTaskErrorCause.USE_BY_CONTENT,
errorMessage: SaveTaskErrorCause.CANCEL,
};
}
const content = res.content;
if (!force) {
const currentMd5 = this.hashCalculateService.calculate(content);
if (change.baseMd5 !== currentMd5) {
return {
state: SaveTaskResponseState.DIFF,
};
}
}
const contentRes = applyChanges(content, change.changes!, change.eol);
const latestStat = await this.fileService.getFileStat(uri);
if (!latestStat || latestStat.lastModification !== mtime) {
throw new Error('File has been modified during saving, please retry');
}
await this.fileService.setContent(latestStat, contentRes, { encoding });
return {
state: SaveTaskResponseState.SUCCESS,
};
} catch (e) {
return {
state: SaveTaskResponseState.ERROR,
Expand Down
Loading