Path: blob/main/src/vs/workbench/api/test/browser/extHostDocumentSaveParticipant.test.ts
5251 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);83// eslint-disable-next-line local/code-no-any-casts84assert.throws(() => { (event.document as any) = null!; });85});86});8788test('event delivery, bad listener', () => {89const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits);9091const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {92throw new Error('💀');93});9495return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(values => {96sub.dispose();9798const [first] = values;99assert.strictEqual(first, false);100});101});102103test('event delivery, bad listener doesn\'t prevent more events', () => {104const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits);105106const sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {107throw new Error('💀');108});109let event: vscode.TextDocumentWillSaveEvent;110const sub2 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {111event = e;112});113114return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => {115sub1.dispose();116sub2.dispose();117118assert.ok(event);119});120});121122test('event delivery, in subscriber order', () => {123const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits);124125let counter = 0;126const sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {127assert.strictEqual(counter++, 0);128});129130const sub2 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {131assert.strictEqual(counter++, 1);132});133134return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => {135sub1.dispose();136sub2.dispose();137});138});139140test('event delivery, ignore bad listeners', async () => {141const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits, { timeout: 5, errors: 1 });142143let callCount = 0;144const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {145callCount += 1;146throw new Error('boom');147});148149await participant.$participateInSave(resource, SaveReason.EXPLICIT);150await participant.$participateInSave(resource, SaveReason.EXPLICIT);151await participant.$participateInSave(resource, SaveReason.EXPLICIT);152await participant.$participateInSave(resource, SaveReason.EXPLICIT);153154sub.dispose();155assert.strictEqual(callCount, 2);156});157158test('event delivery, overall timeout', async function () {159const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits, { timeout: 20, errors: 5 });160161// let callCount = 0;162const calls: number[] = [];163const sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {164calls.push(1);165});166167const sub2 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {168calls.push(2);169event.waitUntil(timeout(100));170});171172const sub3 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {173calls.push(3);174});175176const values = await participant.$participateInSave(resource, SaveReason.EXPLICIT);177sub1.dispose();178sub2.dispose();179sub3.dispose();180assert.deepStrictEqual(calls, [1, 2]);181assert.strictEqual(values.length, 2);182});183184test('event delivery, waitUntil', () => {185const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits);186187const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {188189event.waitUntil(timeout(10));190event.waitUntil(timeout(10));191event.waitUntil(timeout(10));192});193194return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => {195sub.dispose();196});197198});199200test('event delivery, waitUntil must be called sync', () => {201const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits);202203const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {204205event.waitUntil(new Promise<undefined>((resolve, reject) => {206setTimeout(() => {207try {208assert.throws(() => event.waitUntil(timeout(10)));209resolve(undefined);210} catch (e) {211reject(e);212}213214}, 10);215}));216});217218return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => {219sub.dispose();220});221});222223test('event delivery, waitUntil will timeout', function () {224225const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits, { timeout: 5, errors: 3 });226227const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) {228event.waitUntil(timeout(100));229});230231return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(values => {232sub.dispose();233234const [first] = values;235assert.strictEqual(first, false);236});237});238239test('event delivery, waitUntil failure handling', () => {240const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits);241242const sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {243e.waitUntil(Promise.reject(new Error('dddd')));244});245246let event: vscode.TextDocumentWillSaveEvent;247const sub2 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {248event = e;249});250251return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => {252assert.ok(event);253sub1.dispose();254sub2.dispose();255});256});257258test('event delivery, pushEdits sync', () => {259260let dto: IWorkspaceEditDto;261const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, new class extends mock<MainThreadTextEditorsShape>() {262$tryApplyWorkspaceEdit(_edits: SerializableObjectWithBuffers<IWorkspaceEditDto>) {263dto = _edits.value;264return Promise.resolve(true);265}266});267268const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {269e.waitUntil(Promise.resolve([TextEdit.insert(new Position(0, 0), 'bar')]));270e.waitUntil(Promise.resolve([TextEdit.setEndOfLine(EndOfLine.CRLF)]));271});272273return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => {274sub.dispose();275276assert.strictEqual(dto.edits.length, 2);277assert.ok((<IWorkspaceTextEditDto>dto.edits[0]).textEdit);278assert.ok((<IWorkspaceTextEditDto>dto.edits[1]).textEdit);279});280});281282test('event delivery, concurrent change', () => {283284let edits: IWorkspaceEditDto;285const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, new class extends mock<MainThreadTextEditorsShape>() {286$tryApplyWorkspaceEdit(_edits: SerializableObjectWithBuffers<IWorkspaceEditDto>) {287edits = _edits.value;288return Promise.resolve(true);289}290});291292const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {293294// concurrent change from somewhere295documents.$acceptModelChanged(resource, {296changes: [{297range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 },298rangeOffset: undefined!,299rangeLength: undefined!,300text: 'bar'301}],302eol: undefined!,303versionId: 2,304isRedoing: false,305isUndoing: false,306detailedReason: undefined,307isFlush: false,308isEolChange: false,309}, true);310311e.waitUntil(Promise.resolve([TextEdit.insert(new Position(0, 0), 'bar')]));312});313314return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(values => {315sub.dispose();316317assert.strictEqual(edits, undefined);318assert.strictEqual(values[0], false);319});320321});322323test('event delivery, two listeners -> two document states', () => {324325const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, new class extends mock<MainThreadTextEditorsShape>() {326$tryApplyWorkspaceEdit(dto: SerializableObjectWithBuffers<IWorkspaceEditDto>) {327328for (const edit of dto.value.edits) {329330const uri = URI.revive((<IWorkspaceTextEditDto>edit).resource);331const { text, range } = (<IWorkspaceTextEditDto>edit).textEdit;332documents.$acceptModelChanged(uri, {333changes: [{334range,335text,336rangeOffset: undefined!,337rangeLength: undefined!,338}],339eol: undefined!,340versionId: documents.getDocumentData(uri)!.version + 1,341isRedoing: false,342isUndoing: false,343detailedReason: undefined,344isFlush: false,345isEolChange: false,346}, true);347// }348}349350return Promise.resolve(true);351}352});353354const document = documents.getDocument(resource);355356const sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {357// the document state we started with358assert.strictEqual(document.version, 1);359assert.strictEqual(document.getText(), 'foo');360361e.waitUntil(Promise.resolve([TextEdit.insert(new Position(0, 0), 'bar')]));362});363364const sub2 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {365// the document state AFTER the first listener kicked in366assert.strictEqual(document.version, 2);367assert.strictEqual(document.getText(), 'barfoo');368369e.waitUntil(Promise.resolve([TextEdit.insert(new Position(0, 0), 'bar')]));370});371372return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(values => {373sub1.dispose();374sub2.dispose();375376// the document state AFTER eventing is done377assert.strictEqual(document.version, 3);378assert.strictEqual(document.getText(), 'barbarfoo');379});380381});382383test('Log failing listener', function () {384let didLogSomething = false;385const participant = new ExtHostDocumentSaveParticipant(new class extends NullLogService {386override error(message: string | Error, ...args: any[]): void {387didLogSomething = true;388}389}, documents, mainThreadBulkEdits);390391392const sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) {393throw new Error('boom');394});395396return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => {397sub.dispose();398assert.strictEqual(didLogSomething, true);399});400});401});402403404