Path: blob/main/src/vs/workbench/api/test/browser/mainThreadEditors.test.ts
5240 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*--------------------------------------------------------------------------------------------*/45import assert from 'assert';6import { Event } from '../../../../base/common/event.js';7import { DisposableStore, IReference, ImmortalReference } from '../../../../base/common/lifecycle.js';8import { URI } from '../../../../base/common/uri.js';9import { mock } from '../../../../base/test/common/mock.js';10import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';11import { IBulkEditService } from '../../../../editor/browser/services/bulkEditService.js';12import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';13import { EditOperation, ISingleEditOperation } from '../../../../editor/common/core/editOperation.js';14import { Position } from '../../../../editor/common/core/position.js';15import { Range } from '../../../../editor/common/core/range.js';16import { ILanguageService } from '../../../../editor/common/languages/language.js';17import { ILanguageConfigurationService } from '../../../../editor/common/languages/languageConfigurationRegistry.js';18import { EndOfLineSequence, ITextSnapshot } from '../../../../editor/common/model.js';19import { IEditorWorkerService } from '../../../../editor/common/services/editorWorker.js';20import { LanguageService } from '../../../../editor/common/services/languageService.js';21import { IModelService } from '../../../../editor/common/services/model.js';22import { ModelService } from '../../../../editor/common/services/modelService.js';23import { IResolvedTextEditorModel, ITextModelService } from '../../../../editor/common/services/resolverService.js';24import { ITreeSitterLibraryService } from '../../../../editor/common/services/treeSitter/treeSitterLibraryService.js';25import { TestCodeEditorService } from '../../../../editor/test/browser/editorTestServices.js';26import { TestLanguageConfigurationService } from '../../../../editor/test/common/modes/testLanguageConfigurationService.js';27import { TestTreeSitterLibraryService } from '../../../../editor/test/common/services/testTreeSitterLibraryService.js';28import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';29import { TestConfigurationService } from '../../../../platform/configuration/test/common/testConfigurationService.js';30import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';31import { TestDialogService } from '../../../../platform/dialogs/test/common/testDialogService.js';32import { IEnvironmentService } from '../../../../platform/environment/common/environment.js';33import { IFileService } from '../../../../platform/files/common/files.js';34import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js';35import { InstantiationService } from '../../../../platform/instantiation/common/instantiationService.js';36import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js';37import { ILabelService } from '../../../../platform/label/common/label.js';38import { ILogService, NullLogService } from '../../../../platform/log/common/log.js';39import { INotificationService } from '../../../../platform/notification/common/notification.js';40import { TestNotificationService } from '../../../../platform/notification/test/common/testNotificationService.js';41import { TestThemeService } from '../../../../platform/theme/test/common/testThemeService.js';42import { IUndoRedoService } from '../../../../platform/undoRedo/common/undoRedo.js';43import { UndoRedoService } from '../../../../platform/undoRedo/common/undoRedoService.js';44import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';45import { UriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentityService.js';46import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';47import { BulkEditService } from '../../../contrib/bulkEdit/browser/bulkEditService.js';48import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js';49import { IEditorService } from '../../../services/editor/common/editorService.js';50import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js';51import { SerializableObjectWithBuffers } from '../../../services/extensions/common/proxyIdentifier.js';52import { LabelService } from '../../../services/label/common/labelService.js';53import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js';54import { IPaneCompositePartService } from '../../../services/panecomposite/browser/panecomposite.js';55import { ITextFileService } from '../../../services/textfile/common/textfiles.js';56import { ICopyOperation, ICreateFileOperation, ICreateOperation, IDeleteOperation, IMoveOperation, IWorkingCopyFileService } from '../../../services/workingCopy/common/workingCopyFileService.js';57import { IWorkingCopyService } from '../../../services/workingCopy/common/workingCopyService.js';58import { TestEditorGroupsService, TestEditorService, TestEnvironmentService, TestLifecycleService, TestWorkingCopyService } from '../../../test/browser/workbenchTestServices.js';59import { TestContextService, TestFileService, TestTextResourcePropertiesService } from '../../../test/common/workbenchTestServices.js';60import { MainThreadBulkEdits } from '../../browser/mainThreadBulkEdits.js';61import { MainThreadTextEditors, IMainThreadEditorLocator } from '../../browser/mainThreadEditors.js';62import { MainThreadTextEditor } from '../../browser/mainThreadEditor.js';63import { MainThreadDocuments } from '../../browser/mainThreadDocuments.js';64import { IWorkspaceTextEditDto } from '../../common/extHost.protocol.js';65import { SingleProxyRPCProtocol } from '../common/testRPCProtocol.js';66import { ITextResourcePropertiesService } from '../../../../editor/common/services/textResourceConfiguration.js';67import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';68import { TestClipboardService } from '../../../../platform/clipboard/test/common/testClipboardService.js';69import { createTestCodeEditor } from '../../../../editor/test/browser/testCodeEditor.js';7071suite('MainThreadEditors', () => {7273let disposables: DisposableStore;74const existingResource = URI.parse('foo:existing');75const resource = URI.parse('foo:bar');7677let modelService: IModelService;7879let bulkEdits: MainThreadBulkEdits;80let editors: MainThreadTextEditors;81let editorLocator: IMainThreadEditorLocator;82let testEditor: MainThreadTextEditor;8384const movedResources = new Map<URI, URI>();85const copiedResources = new Map<URI, URI>();86const createdResources = new Set<URI>();87const deletedResources = new Set<URI>();8889const editorId = 'testEditorId';9091setup(() => {92disposables = new DisposableStore();9394movedResources.clear();95copiedResources.clear();96createdResources.clear();97deletedResources.clear();9899const configService = new TestConfigurationService();100const dialogService = new TestDialogService();101const notificationService = new TestNotificationService();102const undoRedoService = new UndoRedoService(dialogService, notificationService);103const themeService = new TestThemeService();104105const services = new ServiceCollection();106services.set(IBulkEditService, new SyncDescriptor(BulkEditService));107services.set(ILabelService, new SyncDescriptor(LabelService));108services.set(ILogService, new NullLogService());109services.set(IWorkspaceContextService, new TestContextService());110services.set(IEnvironmentService, TestEnvironmentService);111services.set(IWorkbenchEnvironmentService, TestEnvironmentService);112services.set(IConfigurationService, configService);113services.set(IDialogService, dialogService);114services.set(INotificationService, notificationService);115services.set(IUndoRedoService, undoRedoService);116services.set(ITextResourcePropertiesService, new SyncDescriptor(TestTextResourcePropertiesService));117services.set(IModelService, new SyncDescriptor(ModelService));118services.set(ICodeEditorService, new TestCodeEditorService(themeService));119services.set(IFileService, new TestFileService());120services.set(IUriIdentityService, new SyncDescriptor(UriIdentityService));121services.set(ITreeSitterLibraryService, new TestTreeSitterLibraryService());122services.set(IEditorService, disposables.add(new TestEditorService()));123services.set(ILifecycleService, new TestLifecycleService());124services.set(IWorkingCopyService, new TestWorkingCopyService());125services.set(IEditorGroupsService, new TestEditorGroupsService());126services.set(IClipboardService, new TestClipboardService());127services.set(ITextFileService, new class extends mock<ITextFileService>() {128override isDirty() { return false; }129// eslint-disable-next-line local/code-no-any-casts130override files = <any>{131onDidSave: Event.None,132onDidRevert: Event.None,133onDidChangeDirty: Event.None,134onDidChangeEncoding: Event.None135};136// eslint-disable-next-line local/code-no-any-casts137override untitled = <any>{138onDidChangeEncoding: Event.None139};140override create(operations: { resource: URI }[]) {141for (const o of operations) {142createdResources.add(o.resource);143}144return Promise.resolve(Object.create(null));145}146override async getEncodedReadable(resource: URI, value?: string | ITextSnapshot): Promise<any> {147return undefined;148}149});150services.set(IWorkingCopyFileService, new class extends mock<IWorkingCopyFileService>() {151override onDidRunWorkingCopyFileOperation = Event.None;152override createFolder(operations: ICreateOperation[]): any {153this.create(operations);154}155override create(operations: ICreateFileOperation[]) {156for (const operation of operations) {157createdResources.add(operation.resource);158}159return Promise.resolve(Object.create(null));160}161override move(operations: IMoveOperation[]) {162const { source, target } = operations[0].file;163movedResources.set(source, target);164return Promise.resolve(Object.create(null));165}166override copy(operations: ICopyOperation[]) {167const { source, target } = operations[0].file;168copiedResources.set(source, target);169return Promise.resolve(Object.create(null));170}171override delete(operations: IDeleteOperation[]) {172for (const operation of operations) {173deletedResources.add(operation.resource);174}175return Promise.resolve(undefined);176}177});178services.set(ITextModelService, new class extends mock<ITextModelService>() {179override createModelReference(resource: URI): Promise<IReference<IResolvedTextEditorModel>> {180const textEditorModel = new class extends mock<IResolvedTextEditorModel>() {181override textEditorModel = modelService.getModel(resource)!;182};183textEditorModel.isReadonly = () => false;184return Promise.resolve(new ImmortalReference(textEditorModel));185}186});187services.set(IEditorWorkerService, new class extends mock<IEditorWorkerService>() {188189});190services.set(IPaneCompositePartService, new class extends mock<IPaneCompositePartService>() implements IPaneCompositePartService {191override onDidPaneCompositeOpen = Event.None;192override onDidPaneCompositeClose = Event.None;193override getActivePaneComposite() {194return undefined;195}196});197198services.set(ILanguageService, disposables.add(new LanguageService()));199services.set(ILanguageConfigurationService, new TestLanguageConfigurationService());200201const instaService = new InstantiationService(services);202203bulkEdits = instaService.createInstance(MainThreadBulkEdits, SingleProxyRPCProtocol(null));204const documents = instaService.createInstance(MainThreadDocuments, SingleProxyRPCProtocol(null));205206// Create editor locator207editorLocator = {208getEditor(id: string): MainThreadTextEditor | undefined {209return id === editorId ? testEditor : undefined;210},211findTextEditorIdFor() { return undefined; },212getIdOfCodeEditor() { return undefined; }213};214215editors = instaService.createInstance(MainThreadTextEditors, editorLocator, SingleProxyRPCProtocol(null));216modelService = instaService.invokeFunction(accessor => accessor.get(IModelService));217218// Create a test code editor using the helper219const model = modelService.createModel('Hello world!', null, existingResource);220const testCodeEditor = disposables.add(createTestCodeEditor(model));221222testEditor = disposables.add(instaService.createInstance(223MainThreadTextEditor,224editorId,225model,226testCodeEditor,227{ onGainedFocus() { }, onLostFocus() { } },228documents229));230});231232teardown(() => {233disposables.dispose();234});235236ensureNoDisposablesAreLeakedInTestSuite();237238test(`applyWorkspaceEdit returns false if model is changed by user`, () => {239240const model = disposables.add(modelService.createModel('something', null, resource));241242const workspaceResourceEdit: IWorkspaceTextEditDto = {243resource: resource,244versionId: model.getVersionId(),245textEdit: {246text: 'asdfg',247range: new Range(1, 1, 1, 1)248}249};250251// Act as if the user edited the model252model.applyEdits([EditOperation.insert(new Position(0, 0), 'something')]);253254return bulkEdits.$tryApplyWorkspaceEdit(new SerializableObjectWithBuffers({ edits: [workspaceResourceEdit] })).then((result) => {255assert.strictEqual(result, false);256});257});258259test(`issue #54773: applyWorkspaceEdit checks model version in race situation`, () => {260261const model = disposables.add(modelService.createModel('something', null, resource));262263const workspaceResourceEdit1: IWorkspaceTextEditDto = {264resource: resource,265versionId: model.getVersionId(),266textEdit: {267text: 'asdfg',268range: new Range(1, 1, 1, 1)269}270};271const workspaceResourceEdit2: IWorkspaceTextEditDto = {272resource: resource,273versionId: model.getVersionId(),274textEdit: {275text: 'asdfg',276range: new Range(1, 1, 1, 1)277}278};279280const p1 = bulkEdits.$tryApplyWorkspaceEdit(new SerializableObjectWithBuffers({ edits: [workspaceResourceEdit1] })).then((result) => {281// first edit request succeeds282assert.strictEqual(result, true);283});284const p2 = bulkEdits.$tryApplyWorkspaceEdit(new SerializableObjectWithBuffers({ edits: [workspaceResourceEdit2] })).then((result) => {285// second edit request fails286assert.strictEqual(result, false);287});288return Promise.all([p1, p2]);289});290291test('applyWorkspaceEdit: noop eol edit keeps undo stack clean', async () => {292293const initialText = 'hello\nworld';294const model = disposables.add(modelService.createModel(initialText, null, resource));295const initialAlternativeVersionId = model.getAlternativeVersionId();296297const insertEdit: IWorkspaceTextEditDto = {298resource: resource,299versionId: model.getVersionId(),300textEdit: {301range: new Range(1, 6, 1, 6),302text: '2'303}304};305306const insertResult = await bulkEdits.$tryApplyWorkspaceEdit(new SerializableObjectWithBuffers({ edits: [insertEdit] }));307assert.strictEqual(insertResult, true);308assert.strictEqual(model.getValue(), 'hello2\nworld');309assert.notStrictEqual(model.getAlternativeVersionId(), initialAlternativeVersionId);310311const eolEdit: IWorkspaceTextEditDto = {312resource: resource,313versionId: model.getVersionId(),314textEdit: {315range: new Range(1, 1, 1, 1),316text: '',317eol: EndOfLineSequence.LF318}319};320321const eolResult = await bulkEdits.$tryApplyWorkspaceEdit(new SerializableObjectWithBuffers({ edits: [eolEdit] }));322assert.strictEqual(eolResult, true);323assert.strictEqual(model.getValue(), 'hello2\nworld');324325const undoResult = model.undo();326if (undoResult) {327await undoResult;328}329assert.strictEqual(model.getValue(), initialText);330assert.strictEqual(model.getAlternativeVersionId(), initialAlternativeVersionId);331});332333test(`applyWorkspaceEdit with only resource edit`, () => {334return bulkEdits.$tryApplyWorkspaceEdit(new SerializableObjectWithBuffers({335edits: [336{ oldResource: resource, newResource: resource, options: undefined },337{ oldResource: undefined, newResource: resource, options: undefined },338{ oldResource: resource, newResource: undefined, options: undefined }339]340})).then((result) => {341assert.strictEqual(result, true);342assert.strictEqual(movedResources.get(resource), resource);343assert.strictEqual(createdResources.has(resource), true);344assert.strictEqual(deletedResources.has(resource), true);345});346});347348test('applyWorkspaceEdit can control undo/redo stack 1', async () => {349const model = modelService.getModel(existingResource)!;350351const edit1: ISingleEditOperation = {352range: new Range(1, 1, 1, 2),353text: 'h',354forceMoveMarkers: false355};356357const applied1 = await editors.$tryApplyEdits(editorId, model.getVersionId(), [edit1], { undoStopBefore: false, undoStopAfter: false });358assert.strictEqual(applied1, true);359assert.strictEqual(model.getValue(), 'hello world!');360361const edit2: ISingleEditOperation = {362range: new Range(1, 2, 1, 6),363text: 'ELLO',364forceMoveMarkers: false365};366367const applied2 = await editors.$tryApplyEdits(editorId, model.getVersionId(), [edit2], { undoStopBefore: false, undoStopAfter: false });368assert.strictEqual(applied2, true);369assert.strictEqual(model.getValue(), 'hELLO world!');370371await model.undo();372assert.strictEqual(model.getValue(), 'Hello world!');373});374375test('applyWorkspaceEdit can control undo/redo stack 2', async () => {376const model = modelService.getModel(existingResource)!;377378const edit1: ISingleEditOperation = {379range: new Range(1, 1, 1, 2),380text: 'h',381forceMoveMarkers: false382};383384const applied1 = await editors.$tryApplyEdits(editorId, model.getVersionId(), [edit1], { undoStopBefore: false, undoStopAfter: false });385assert.strictEqual(applied1, true);386assert.strictEqual(model.getValue(), 'hello world!');387388const edit2: ISingleEditOperation = {389range: new Range(1, 2, 1, 6),390text: 'ELLO',391forceMoveMarkers: false392};393394const applied2 = await editors.$tryApplyEdits(editorId, model.getVersionId(), [edit2], { undoStopBefore: true, undoStopAfter: false });395assert.strictEqual(applied2, true);396assert.strictEqual(model.getValue(), 'hELLO world!');397398await model.undo();399assert.strictEqual(model.getValue(), 'hello world!');400});401});402403404