Path: blob/main/src/vs/workbench/api/test/browser/extHostDocumentSaveParticipant.test.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/4import assert from 'assert';5import { URI } from '../../../../base/common/uri.js';6import { ExtHostDocuments } from '../../common/extHostDocuments.js';7import { ExtHostDocumentsAndEditors } from '../../common/extHostDocumentsAndEditors.js';8import { TextDocumentSaveReason, TextEdit, Position, EndOfLine } from '../../common/extHostTypes.js';9import { MainThreadTextEditorsShape, IWorkspaceEditDto, IWorkspaceTextEditDto, MainThreadBulkEditsShape } from '../../common/extHost.protocol.js';10import { ExtHostDocumentSaveParticipant } from '../../common/extHostDocumentSaveParticipant.js';11import { SingleProxyRPCProtocol } from '../common/testRPCProtocol.js';12import { SaveReason } from '../../../common/editor.js';13import type * as vscode from 'vscode';14import { mock } from '../../../../base/test/common/mock.js';15import { NullLogService } from '../../../../platform/log/common/log.js';16import { nullExtensionDescription } from '../../../services/extensions/common/extensions.js';17import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';18import { SerializableObjectWithBuffers } from '../../../services/extensions/common/proxyIdentifier.js';1920function timeout(n: number) {21return new Promise(resolve => setTimeout(resolve, n));22}2324suite('ExtHostDocumentSaveParticipant', () => {2526const resource = URI.parse('foo:bar');27const mainThreadBulkEdits = new class extends mock<MainThreadBulkEditsShape>() { };28let documents: ExtHostDocuments;29const nullLogService = new NullLogService();3031setup(() => {32const documentsAndEditors = new ExtHostDocumentsAndEditors(SingleProxyRPCProtocol(null), new NullLogService());33documentsAndEditors.$acceptDocumentsAndEditorsDelta({34addedDocuments: [{35isDirty: false,36languageId: 'foo',37uri: resource,38versionId: 1,39lines: ['foo'],40EOL: '\n',41encoding: 'utf8'42}]43});44documents = new ExtHostDocuments(SingleProxyRPCProtocol(null), documentsAndEditors);45});4647ensureNoDisposablesAreLeakedInTestSuite();4849test('no listeners, no problem', () => {50const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits);51return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => assert.ok(true));52});5354test('event delivery', () => {55const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits);5657let event: vscode.TextDocumentWillSaveEvent;58const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {59event = e;60});6162return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => {63sub.dispose();6465assert.ok(event);66assert.strictEqual(event.reason, TextDocumentSaveReason.Manual);67assert.strictEqual(typeof event.waitUntil, 'function');68});69});7071test('event delivery, immutable', () => {72const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits);7374let event: vscode.TextDocumentWillSaveEvent;75const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {76event = e;77});7879return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => {80sub.dispose();8182assert.ok(event);83assert.throws(() => { (event.document as any) = null!; });84});85});8687test('event delivery, bad listener', () => {88const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits);8990const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {91throw new Error('💀');92});9394return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(values => {95sub.dispose();9697const [first] = values;98assert.strictEqual(first, false);99});100});101102test('event delivery, bad listener doesn\'t prevent more events', () => {103const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits);104105const sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {106throw new Error('💀');107});108let event: vscode.TextDocumentWillSaveEvent;109const sub2 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {110event = e;111});112113return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => {114sub1.dispose();115sub2.dispose();116117assert.ok(event);118});119});120121test('event delivery, in subscriber order', () => {122const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits);123124let counter = 0;125const sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {126assert.strictEqual(counter++, 0);127});128129const sub2 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {130assert.strictEqual(counter++, 1);131});132133return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => {134sub1.dispose();135sub2.dispose();136});137});138139test('event delivery, ignore bad listeners', async () => {140const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits, { timeout: 5, errors: 1 });141142let callCount = 0;143const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {144callCount += 1;145throw new Error('boom');146});147148await participant.$participateInSave(resource, SaveReason.EXPLICIT);149await participant.$participateInSave(resource, SaveReason.EXPLICIT);150await participant.$participateInSave(resource, SaveReason.EXPLICIT);151await participant.$participateInSave(resource, SaveReason.EXPLICIT);152153sub.dispose();154assert.strictEqual(callCount, 2);155});156157test('event delivery, overall timeout', async function () {158const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits, { timeout: 20, errors: 5 });159160// let callCount = 0;161const calls: number[] = [];162const sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {163calls.push(1);164});165166const sub2 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {167calls.push(2);168event.waitUntil(timeout(100));169});170171const sub3 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {172calls.push(3);173});174175const values = await participant.$participateInSave(resource, SaveReason.EXPLICIT);176sub1.dispose();177sub2.dispose();178sub3.dispose();179assert.deepStrictEqual(calls, [1, 2]);180assert.strictEqual(values.length, 2);181});182183test('event delivery, waitUntil', () => {184const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits);185186const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {187188event.waitUntil(timeout(10));189event.waitUntil(timeout(10));190event.waitUntil(timeout(10));191});192193return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => {194sub.dispose();195});196197});198199test('event delivery, waitUntil must be called sync', () => {200const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits);201202const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {203204event.waitUntil(new Promise<undefined>((resolve, reject) => {205setTimeout(() => {206try {207assert.throws(() => event.waitUntil(timeout(10)));208resolve(undefined);209} catch (e) {210reject(e);211}212213}, 10);214}));215});216217return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => {218sub.dispose();219});220});221222test('event delivery, waitUntil will timeout', function () {223224const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits, { timeout: 5, errors: 3 });225226const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {227event.waitUntil(timeout(100));228});229230return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(values => {231sub.dispose();232233const [first] = values;234assert.strictEqual(first, false);235});236});237238test('event delivery, waitUntil failure handling', () => {239const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits);240241const sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {242e.waitUntil(Promise.reject(new Error('dddd')));243});244245let event: vscode.TextDocumentWillSaveEvent;246const sub2 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {247event = e;248});249250return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => {251assert.ok(event);252sub1.dispose();253sub2.dispose();254});255});256257test('event delivery, pushEdits sync', () => {258259let dto: IWorkspaceEditDto;260const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, new class extends mock<MainThreadTextEditorsShape>() {261$tryApplyWorkspaceEdit(_edits: SerializableObjectWithBuffers<IWorkspaceEditDto>) {262dto = _edits.value;263return Promise.resolve(true);264}265});266267const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {268e.waitUntil(Promise.resolve([TextEdit.insert(new Position(0, 0), 'bar')]));269e.waitUntil(Promise.resolve([TextEdit.setEndOfLine(EndOfLine.CRLF)]));270});271272return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => {273sub.dispose();274275assert.strictEqual(dto.edits.length, 2);276assert.ok((<IWorkspaceTextEditDto>dto.edits[0]).textEdit);277assert.ok((<IWorkspaceTextEditDto>dto.edits[1]).textEdit);278});279});280281test('event delivery, concurrent change', () => {282283let edits: IWorkspaceEditDto;284const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, new class extends mock<MainThreadTextEditorsShape>() {285$tryApplyWorkspaceEdit(_edits: SerializableObjectWithBuffers<IWorkspaceEditDto>) {286edits = _edits.value;287return Promise.resolve(true);288}289});290291const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {292293// concurrent change from somewhere294documents.$acceptModelChanged(resource, {295changes: [{296range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 },297rangeOffset: undefined!,298rangeLength: undefined!,299text: 'bar'300}],301eol: undefined!,302versionId: 2,303isRedoing: false,304isUndoing: false,305detailedReason: undefined,306isFlush: false,307isEolChange: false,308}, true);309310e.waitUntil(Promise.resolve([TextEdit.insert(new Position(0, 0), 'bar')]));311});312313return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(values => {314sub.dispose();315316assert.strictEqual(edits, undefined);317assert.strictEqual(values[0], false);318});319320});321322test('event delivery, two listeners -> two document states', () => {323324const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, new class extends mock<MainThreadTextEditorsShape>() {325$tryApplyWorkspaceEdit(dto: SerializableObjectWithBuffers<IWorkspaceEditDto>) {326327for (const edit of dto.value.edits) {328329const uri = URI.revive((<IWorkspaceTextEditDto>edit).resource);330const { text, range } = (<IWorkspaceTextEditDto>edit).textEdit;331documents.$acceptModelChanged(uri, {332changes: [{333range,334text,335rangeOffset: undefined!,336rangeLength: undefined!,337}],338eol: undefined!,339versionId: documents.getDocumentData(uri)!.version + 1,340isRedoing: false,341isUndoing: false,342detailedReason: undefined,343isFlush: false,344isEolChange: false,345}, true);346// }347}348349return Promise.resolve(true);350}351});352353const document = documents.getDocument(resource);354355const sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {356// the document state we started with357assert.strictEqual(document.version, 1);358assert.strictEqual(document.getText(), 'foo');359360e.waitUntil(Promise.resolve([TextEdit.insert(new Position(0, 0), 'bar')]));361});362363const sub2 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {364// the document state AFTER the first listener kicked in365assert.strictEqual(document.version, 2);366assert.strictEqual(document.getText(), 'barfoo');367368e.waitUntil(Promise.resolve([TextEdit.insert(new Position(0, 0), 'bar')]));369});370371return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(values => {372sub1.dispose();373sub2.dispose();374375// the document state AFTER eventing is done376assert.strictEqual(document.version, 3);377assert.strictEqual(document.getText(), 'barbarfoo');378});379380});381382test('Log failing listener', function () {383let didLogSomething = false;384const participant = new ExtHostDocumentSaveParticipant(new class extends NullLogService {385override error(message: string | Error, ...args: any[]): void {386didLogSomething = true;387}388}, documents, mainThreadBulkEdits);389390391const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {392throw new Error('boom');393});394395return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => {396sub.dispose();397assert.strictEqual(didLogSomething, true);398});399});400});401402403