/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { Handler } from 'vs/editor/common/editorCommon'; import * as modes from 'vs/editor/common/modes'; import { OnTypeRenameContribution } from 'vs/editor/contrib/rename/onTypeRename'; import { createTestCodeEditor, ITestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; import { CoreEditingCommands } from 'vs/editor/browser/controller/coreCommands'; import { ITextModel } from 'vs/editor/common/model'; import { USUAL_WORD_SEPARATORS } from 'vs/editor/common/model/wordHelper'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; const mockFile = URI.parse('test:somefile.ttt'); const mockFileSelector = { scheme: 'test' }; const timeout = 30; interface TestEditor { setPosition(pos: Position): Promise; setSelection(sel: IRange): Promise; trigger(source: string | null | undefined, handlerId: string, payload: any): Promise; undo(): void; redo(): void; } const languageIdentifier = new modes.LanguageIdentifier('onTypeRenameTestLangage', 74); LanguageConfigurationRegistry.register(languageIdentifier, { wordPattern: /[a-zA-Z]+/ }); suite('On type rename', () => { const disposables = new DisposableStore(); setup(() => { disposables.clear(); }); teardown(() => { disposables.clear(); }); function createMockEditor(text: string | string[]): ITestCodeEditor { const model = typeof text === 'string' ? createTextModel(text, undefined, languageIdentifier, mockFile) : createTextModel(text.join('\n'), undefined, languageIdentifier, mockFile); const editor = createTestCodeEditor({ model }); disposables.add(model); disposables.add(editor); return editor; } function testCase( name: string, initialState: { text: string | string[], responseWordPattern?: RegExp }, operations: (editor: TestEditor) => Promise, expectedEndText: string | string[] ) { test(name, async () => { disposables.add(modes.OnTypeRenameProviderRegistry.register(mockFileSelector, { provideOnTypeRenameRanges(model: ITextModel, pos: IPosition) { const wordAtPos = model.getWordAtPosition(pos); if (wordAtPos) { const matches = model.findMatches(wordAtPos.word, false, false, true, USUAL_WORD_SEPARATORS, false); return { ranges: matches.map(m => m.range), wordPattern: initialState.responseWordPattern }; } return { ranges: [], wordPattern: initialState.responseWordPattern }; } })); const editor = createMockEditor(initialState.text); editor.updateOptions({ renameOnType: true }); const ontypeRenameContribution = editor.registerAndInstantiateContribution( OnTypeRenameContribution.ID, OnTypeRenameContribution ); ontypeRenameContribution.setDebounceDuration(0); const testEditor: TestEditor = { setPosition(pos: Position) { editor.setPosition(pos); return ontypeRenameContribution.currentUpdateTriggerPromise; }, setSelection(sel: IRange) { editor.setSelection(sel); return ontypeRenameContribution.currentUpdateTriggerPromise; }, trigger(source: string | null | undefined, handlerId: string, payload: any) { editor.trigger(source, handlerId, payload); return ontypeRenameContribution.currentSyncTriggerPromise; }, undo() { CoreEditingCommands.Undo.runEditorCommand(null, editor, null); }, redo() { CoreEditingCommands.Redo.runEditorCommand(null, editor, null); } }; await operations(testEditor); return new Promise((resolve) => { setTimeout(() => { if (typeof expectedEndText === 'string') { assert.equal(editor.getModel()!.getValue(), expectedEndText); } else { assert.equal(editor.getModel()!.getValue(), expectedEndText.join('\n')); } resolve(); }, timeout); }); }); } const state = { text: '' }; /** * Simple insertion */ testCase('Simple insert - initial', state, async (editor) => { const pos = new Position(1, 2); await editor.setPosition(pos); await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, ''); testCase('Simple insert - middle', state, async (editor) => { const pos = new Position(1, 3); await editor.setPosition(pos); await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, ''); testCase('Simple insert - end', state, async (editor) => { const pos = new Position(1, 5); await editor.setPosition(pos); await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, ''); /** * Simple insertion - end */ testCase('Simple insert end - initial', state, async (editor) => { const pos = new Position(1, 8); await editor.setPosition(pos); await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, ''); testCase('Simple insert end - middle', state, async (editor) => { const pos = new Position(1, 9); await editor.setPosition(pos); await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, ''); testCase('Simple insert end - end', state, async (editor) => { const pos = new Position(1, 11); await editor.setPosition(pos); await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, ''); /** * Boundary insertion */ testCase('Simple insert - out of boundary', state, async (editor) => { const pos = new Position(1, 1); await editor.setPosition(pos); await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, 'i'); testCase('Simple insert - out of boundary 2', state, async (editor) => { const pos = new Position(1, 6); await editor.setPosition(pos); await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, 'i'); testCase('Simple insert - out of boundary 3', state, async (editor) => { const pos = new Position(1, 7); await editor.setPosition(pos); await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, ''); testCase('Simple insert - out of boundary 4', state, async (editor) => { const pos = new Position(1, 12); await editor.setPosition(pos); await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, 'i'); /** * Insert + Move */ testCase('Continuous insert', state, async (editor) => { const pos = new Position(1, 2); await editor.setPosition(pos); await editor.trigger('keyboard', Handler.Type, { text: 'i' }); await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, ''); testCase('Insert - move - insert', state, async (editor) => { const pos = new Position(1, 2); await editor.setPosition(pos); await editor.trigger('keyboard', Handler.Type, { text: 'i' }); await editor.setPosition(new Position(1, 4)); await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, ''); testCase('Insert - move - insert outside region', state, async (editor) => { const pos = new Position(1, 2); await editor.setPosition(pos); await editor.trigger('keyboard', Handler.Type, { text: 'i' }); await editor.setPosition(new Position(1, 7)); await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, 'i'); /** * Selection insert */ testCase('Selection insert - simple', state, async (editor) => { const pos = new Position(1, 2); await editor.setPosition(pos); await editor.setSelection(new Range(1, 2, 1, 3)); await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, ''); testCase('Selection insert - whole', state, async (editor) => { const pos = new Position(1, 2); await editor.setPosition(pos); await editor.setSelection(new Range(1, 2, 1, 5)); await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, ''); testCase('Selection insert - across boundary', state, async (editor) => { const pos = new Position(1, 2); await editor.setPosition(pos); await editor.setSelection(new Range(1, 1, 1, 3)); await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, 'ioo>'); /** * @todo * Undefined behavior */ // testCase('Selection insert - across two boundary', state, async (editor) => { // const pos = new Position(1, 2); // await editor.setPosition(pos); // await ontypeRenameContribution.updateLinkedUI(pos); // await editor.setSelection(new Range(1, 4, 1, 9)); // await editor.trigger('keyboard', Handler.Type, { text: 'i' }); // }, ''); /** * Break out behavior */ testCase('Breakout - type space', state, async (editor) => { const pos = new Position(1, 5); await editor.setPosition(pos); await editor.trigger('keyboard', Handler.Type, { text: ' ' }); }, ''); testCase('Breakout - type space then undo', state, async (editor) => { const pos = new Position(1, 5); await editor.setPosition(pos); await editor.trigger('keyboard', Handler.Type, { text: ' ' }); editor.undo(); }, ''); testCase('Breakout - type space in middle', state, async (editor) => { const pos = new Position(1, 4); await editor.setPosition(pos); await editor.trigger('keyboard', Handler.Type, { text: ' ' }); }, ''); testCase('Breakout - paste content starting with space', state, async (editor) => { const pos = new Position(1, 5); await editor.setPosition(pos); await editor.trigger('keyboard', Handler.Paste, { text: ' i="i"' }); }, ''); testCase('Breakout - paste content starting with space then undo', state, async (editor) => { const pos = new Position(1, 5); await editor.setPosition(pos); await editor.trigger('keyboard', Handler.Paste, { text: ' i="i"' }); editor.undo(); }, ''); testCase('Breakout - paste content starting with space in middle', state, async (editor) => { const pos = new Position(1, 4); await editor.setPosition(pos); await editor.trigger('keyboard', Handler.Paste, { text: ' i' }); }, ''); /** * Break out with custom provider wordPattern */ const state3 = { ...state, responseWordPattern: /[a-yA-Y]+/ }; testCase('Breakout with stop pattern - insert', state3, async (editor) => { const pos = new Position(1, 2); await editor.setPosition(pos); await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, ''); testCase('Breakout with stop pattern - insert stop char', state3, async (editor) => { const pos = new Position(1, 2); await editor.setPosition(pos); await editor.trigger('keyboard', Handler.Type, { text: 'z' }); }, ''); testCase('Breakout with stop pattern - paste char', state3, async (editor) => { const pos = new Position(1, 2); await editor.setPosition(pos); await editor.trigger('keyboard', Handler.Paste, { text: 'z' }); }, ''); testCase('Breakout with stop pattern - paste string', state3, async (editor) => { const pos = new Position(1, 2); await editor.setPosition(pos); await editor.trigger('keyboard', Handler.Paste, { text: 'zo' }); }, ''); testCase('Breakout with stop pattern - insert at end', state3, async (editor) => { const pos = new Position(1, 5); await editor.setPosition(pos); await editor.trigger('keyboard', Handler.Type, { text: 'z' }); }, ''); const state4 = { ...state, responseWordPattern: /[a-eA-E]+/ }; testCase('Breakout with stop pattern - insert stop char, respos', state4, async (editor) => { const pos = new Position(1, 2); await editor.setPosition(pos); await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, ''); /** * Delete */ testCase('Delete - left char', state, async (editor) => { const pos = new Position(1, 5); await editor.setPosition(pos); await editor.trigger('keyboard', 'deleteLeft', {}); }, ''); testCase('Delete - left char then undo', state, async (editor) => { const pos = new Position(1, 5); await editor.setPosition(pos); await editor.trigger('keyboard', 'deleteLeft', {}); editor.undo(); }, ''); testCase('Delete - left word', state, async (editor) => { const pos = new Position(1, 5); await editor.setPosition(pos); await editor.trigger('keyboard', 'deleteWordLeft', {}); }, '<>'); testCase('Delete - left word then undo', state, async (editor) => { const pos = new Position(1, 5); await editor.setPosition(pos); await editor.trigger('keyboard', 'deleteWordLeft', {}); editor.undo(); editor.undo(); }, ''); /** * Todo: Fix test */ // testCase('Delete - left all', state, async (editor) => { // const pos = new Position(1, 3); // await editor.setPosition(pos); // await ontypeRenameContribution.updateLinkedUI(pos); // await editor.trigger('keyboard', 'deleteAllLeft', {}); // }, '>'); /** * Todo: Fix test */ // testCase('Delete - left all then undo', state, async (editor) => { // const pos = new Position(1, 5); // await editor.setPosition(pos); // await ontypeRenameContribution.updateLinkedUI(pos); // await editor.trigger('keyboard', 'deleteAllLeft', {}); // editor.undo(); // }, '>'); testCase('Delete - left all then undo twice', state, async (editor) => { const pos = new Position(1, 5); await editor.setPosition(pos); await editor.trigger('keyboard', 'deleteAllLeft', {}); editor.undo(); editor.undo(); }, ''); testCase('Delete - selection', state, async (editor) => { const pos = new Position(1, 5); await editor.setPosition(pos); await editor.setSelection(new Range(1, 2, 1, 3)); await editor.trigger('keyboard', 'deleteLeft', {}); }, ''); testCase('Delete - selection across boundary', state, async (editor) => { const pos = new Position(1, 3); await editor.setPosition(pos); await editor.setSelection(new Range(1, 1, 1, 3)); await editor.trigger('keyboard', 'deleteLeft', {}); }, 'oo>'); /** * Undo / redo */ testCase('Undo/redo - simple undo', state, async (editor) => { const pos = new Position(1, 2); await editor.setPosition(pos); await editor.trigger('keyboard', Handler.Type, { text: 'i' }); editor.undo(); editor.undo(); }, ''); testCase('Undo/redo - simple undo/redo', state, async (editor) => { const pos = new Position(1, 2); await editor.setPosition(pos); await editor.trigger('keyboard', Handler.Type, { text: 'i' }); editor.undo(); editor.redo(); }, ''); /** * Multi line */ const state2 = { text: [ '', '' ] }; testCase('Multiline insert', state2, async (editor) => { const pos = new Position(1, 2); await editor.setPosition(pos); await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, [ '', '' ]); });