diff --git a/src/sql/workbench/contrib/notebook/browser/markdownToolbarActions.ts b/src/sql/workbench/contrib/notebook/browser/markdownToolbarActions.ts index 257ed782e7..1ab6450929 100644 --- a/src/sql/workbench/contrib/notebook/browser/markdownToolbarActions.ts +++ b/src/sql/workbench/contrib/notebook/browser/markdownToolbarActions.ts @@ -46,8 +46,11 @@ export class TransformMarkdownAction extends Action { export class MarkdownTextTransformer { - private _notebookEditor: INotebookEditor; - constructor(private _notebookService: INotebookService, private _cellModel: ICellModel) { } + constructor(private _notebookService: INotebookService, private _cellModel: ICellModel, private _notebookEditor?: INotebookEditor) { } + + public get notebookEditor(): INotebookEditor { + return this._notebookEditor; + } public transformText(type: MarkdownButtonType): void { let editorControl = this.getEditorControl(); diff --git a/src/sql/workbench/contrib/notebook/test/browser/MarkdownTextTransformer.test.ts b/src/sql/workbench/contrib/notebook/test/browser/MarkdownTextTransformer.test.ts new file mode 100644 index 0000000000..bf7e59b022 --- /dev/null +++ b/src/sql/workbench/contrib/notebook/test/browser/MarkdownTextTransformer.test.ts @@ -0,0 +1,166 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as TypeMoq from 'typemoq'; +import * as assert from 'assert'; + +import { MarkdownTextTransformer, MarkdownButtonType } from 'sql/workbench/contrib/notebook/browser/markdownToolbarActions'; +import { NotebookService } from 'sql/workbench/services/notebook/browser/notebookServiceImpl'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { TestLifecycleService, TestEnvironmentService, TestAccessibilityService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { INotebookService } from 'sql/workbench/services/notebook/browser/notebookService'; +import { CellModel } from 'sql/workbench/services/notebook/browser/models/cell'; +import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; +import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; +import { TextModel } from 'vs/editor/common/model/textModel'; +import { isUndefinedOrNull } from 'vs/base/common/types'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { TestCodeEditorService } from 'vs/editor/test/browser/editorTestServices'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { DefaultEndOfLine } from 'vs/editor/common/model'; +import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; +import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; +import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { IEditor } from 'vs/editor/common/editorCommon'; +import { TestNotebookEditor } from 'sql/workbench/contrib/notebook/test/browser/testEditorsAndProviders.test'; + + + +suite('MarkdownTextTransformer', () => { + let markdownTextTransformer: MarkdownTextTransformer; + let widget: IEditor; + let textModel: TextModel; + + setup(() => { + let mockNotebookService: TypeMoq.Mock; + const dialogService = new TestDialogService(); + const notificationService = new TestNotificationService(); + const undoRedoService = new UndoRedoService(dialogService, notificationService); + const instantiationService = new TestInstantiationService(); + + instantiationService.stub(IAccessibilityService, new TestAccessibilityService()); + instantiationService.stub(IContextKeyService, new MockContextKeyService()); + instantiationService.stub(ICodeEditorService, new TestCodeEditorService()); + instantiationService.stub(IThemeService, new TestThemeService()); + instantiationService.stub(IEnvironmentService, TestEnvironmentService); + instantiationService.stub(IStorageService, new TestStorageService()); + + mockNotebookService = TypeMoq.Mock.ofType(NotebookService, undefined, new TestLifecycleService(), undefined, undefined, undefined, instantiationService, new MockContextKeyService(), + undefined, undefined, undefined, undefined, undefined, undefined, TestEnvironmentService); + + let cellModel = new CellModel(undefined, undefined, mockNotebookService.object); + let notebookEditor = new TestNotebookEditor(cellModel.cellGuid, instantiationService); + markdownTextTransformer = new MarkdownTextTransformer(mockNotebookService.object, cellModel, notebookEditor); + mockNotebookService.setup(s => s.findNotebookEditor(TypeMoq.It.isAny())).returns(() => notebookEditor); + + let editor = notebookEditor.cellEditors[0].getEditor(); + assert(!isUndefinedOrNull(editor), 'editor is undefined'); + + widget = editor.getControl(); + assert(!isUndefinedOrNull(widget), 'widget is undefined'); + + // Create new text model + textModel = new TextModel('', { isForSimpleWidget: true, defaultEOL: DefaultEndOfLine.LF, detectIndentation: true, indentSize: 0, insertSpaces: false, largeFileOptimizations: false, tabSize: 4, trimAutoWhitespace: false }, null, undefined, undoRedoService); + + // Couple widget with newly created text model + widget.setModel(textModel); + + // let textModel = widget.getModel() as TextModel; + assert(!isUndefinedOrNull(widget.getModel()), 'Text model is undefined'); + }); + + test('Transform text with no previous selection', () => { + testWithNoSelection(MarkdownButtonType.BOLD, '****', true); + testWithNoSelection(MarkdownButtonType.BOLD, ''); + testWithNoSelection(MarkdownButtonType.ITALIC, '__', true); + testWithNoSelection(MarkdownButtonType.ITALIC, ''); + // testWithNoSelection(MarkdownButtonType.CODE, '```\n\n```', true); + // testWithNoSelection(MarkdownButtonType.CODE, '\n'); + testWithNoSelection(MarkdownButtonType.HIGHLIGHT, '', true); + testWithNoSelection(MarkdownButtonType.HIGHLIGHT, ''); + testWithNoSelection(MarkdownButtonType.LINK, '[]()', true); + testWithNoSelection(MarkdownButtonType.LINK, ''); + testWithNoSelection(MarkdownButtonType.UNORDERED_LIST, '- ', true); + testWithNoSelection(MarkdownButtonType.UNORDERED_LIST, '- '); + testWithNoSelection(MarkdownButtonType.ORDERED_LIST, '1. ', true); + testWithNoSelection(MarkdownButtonType.ORDERED_LIST, '1. '); + testWithNoSelection(MarkdownButtonType.IMAGE, '![]()', true); + testWithNoSelection(MarkdownButtonType.IMAGE, ''); + }); + + test('Transform text with one word selected', () => { + testWithSingleWordSelected(MarkdownButtonType.BOLD, '**WORD**'); + testWithSingleWordSelected(MarkdownButtonType.ITALIC, '_WORD_'); + testWithSingleWordSelected(MarkdownButtonType.CODE, '```\nWORD\n```'); + testWithSingleWordSelected(MarkdownButtonType.HIGHLIGHT, 'WORD'); + testWithSingleWordSelected(MarkdownButtonType.LINK, '[WORD]()'); + testWithSingleWordSelected(MarkdownButtonType.UNORDERED_LIST, '- WORD'); + testWithSingleWordSelected(MarkdownButtonType.ORDERED_LIST, '1. WORD'); + testWithSingleWordSelected(MarkdownButtonType.IMAGE, '![WORD]()'); + }); + + test('Transform text with multiple words selected', () => { + testWithMultipleWordsSelected(MarkdownButtonType.BOLD, '**Multi Words**'); + testWithMultipleWordsSelected(MarkdownButtonType.ITALIC, '_Multi Words_'); + testWithMultipleWordsSelected(MarkdownButtonType.CODE, '```\nMulti Words\n```'); + testWithMultipleWordsSelected(MarkdownButtonType.HIGHLIGHT, 'Multi Words'); + testWithMultipleWordsSelected(MarkdownButtonType.LINK, '[Multi Words]()'); + testWithMultipleWordsSelected(MarkdownButtonType.UNORDERED_LIST, '- Multi Words'); + testWithMultipleWordsSelected(MarkdownButtonType.ORDERED_LIST, '1. Multi Words'); + testWithMultipleWordsSelected(MarkdownButtonType.IMAGE, '![Multi Words]()'); + }); + + test('Transform text with multiple lines selected', () => { + testWithMultipleLinesSelected(MarkdownButtonType.BOLD, '**Multi\nLines\nSelected**'); + testWithMultipleLinesSelected(MarkdownButtonType.ITALIC, '_Multi\nLines\nSelected_'); + testWithMultipleLinesSelected(MarkdownButtonType.CODE, '```\nMulti\nLines\nSelected\n```'); + testWithMultipleLinesSelected(MarkdownButtonType.HIGHLIGHT, 'Multi\nLines\nSelected'); + testWithMultipleLinesSelected(MarkdownButtonType.LINK, '[Multi\nLines\nSelected]()'); + testWithMultipleLinesSelected(MarkdownButtonType.UNORDERED_LIST, '- Multi\n- Lines\n- Selected'); + testWithMultipleLinesSelected(MarkdownButtonType.ORDERED_LIST, '1. Multi\n1. Lines\n1. Selected'); + testWithMultipleLinesSelected(MarkdownButtonType.IMAGE, '![Multi\nLines\nSelected]()'); + }); + + function testWithNoSelection(type: MarkdownButtonType, expectedValue: string, setValue = false): void { + if (setValue) { + textModel.setValue(''); + } + markdownTextTransformer.transformText(type); + assert.equal(textModel.getValue(), expectedValue, `${MarkdownButtonType[type]} with no selection failed`); + } + + function testWithSingleWordSelected(type: MarkdownButtonType, expectedValue: string): void { + let value = 'WORD'; + textModel.setValue(value); + widget.setSelection({ startColumn: 1, startLineNumber: 1, endColumn: 5, endLineNumber: 1 }); + assert.equal(textModel.getValueInRange(widget.getSelection()), value, 'Expected selection is not found'); + markdownTextTransformer.transformText(type); + assert.equal(textModel.getValue(), expectedValue, `${MarkdownButtonType[type]} with single word selection failed`); + } + + function testWithMultipleWordsSelected(type: MarkdownButtonType, expectedValue: string): void { + let value = 'Multi Words'; + textModel.setValue(value); + widget.setSelection({ startColumn: 1, startLineNumber: 1, endColumn: 12, endLineNumber: 1 }); + assert.equal(textModel.getValueInRange(widget.getSelection()), value, 'Expected multi-word selection is not found'); + markdownTextTransformer.transformText(type); + assert.equal(textModel.getValue(), expectedValue, `${MarkdownButtonType[type]} with multiple word selection failed`); + } + + function testWithMultipleLinesSelected(type: MarkdownButtonType, expectedValue: string): void { + let value = 'Multi\nLines\nSelected'; + textModel.setValue(value); + widget.setSelection({ startColumn: 1, startLineNumber: 1, endColumn: 9, endLineNumber: 3 }); + assert.equal(textModel.getValueInRange(widget.getSelection()), value, 'Expected multi-line selection is not found'); + markdownTextTransformer.transformText(type); + assert.equal(textModel.getValue(), expectedValue, `${MarkdownButtonType[type]} with multiple line selection failed`); + } +}); + diff --git a/src/sql/workbench/contrib/notebook/test/browser/testEditorsAndProviders.test.ts b/src/sql/workbench/contrib/notebook/test/browser/testEditorsAndProviders.test.ts new file mode 100644 index 0000000000..436ab782eb --- /dev/null +++ b/src/sql/workbench/contrib/notebook/test/browser/testEditorsAndProviders.test.ts @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { NotebookEditorStub, CellEditorProviderStub } from 'sql/workbench/contrib/notebook/test/stubs'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { QueryTextEditor } from 'sql/workbench/browser/modelComponents/queryTextEditor'; +import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; +import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; +import { TestTextResourceConfigurationService, TestEditorGroupsService, TestEditorService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import * as dom from 'vs/base/browser/dom'; + +export class TestNotebookEditor extends NotebookEditorStub { + constructor(private _cellGuid?: string, private _instantiationService?: IInstantiationService) { + super(); + } + cellEditors: CellEditorProviderStub[] = [new TestCellEditorProvider(this._cellGuid, this._instantiationService)]; +} + +class TestCellEditorProvider extends CellEditorProviderStub { + private _editor: QueryTextEditor; + private _cellGuid: string; + constructor(cellGuid: string, instantiationService?: IInstantiationService) { + super(); + let div = dom.$('div', undefined, dom.$('span', { id: 'demospan' })); + let firstChild = div.firstChild as HTMLElement; + + this._editor = new QueryTextEditor( + NullTelemetryService, + instantiationService, + new TestStorageService(), + new TestTextResourceConfigurationService(), + new TestThemeService(), + new TestEditorGroupsService(), + new TestEditorService(), + new TestConfigurationService() + ); + this._editor.create(firstChild); + this._cellGuid = cellGuid; + } + cellGuid(): string { + return this._cellGuid; + } + getEditor(): QueryTextEditor { + return this._editor; + } +} diff --git a/src/sql/workbench/contrib/notebook/test/stubs.ts b/src/sql/workbench/contrib/notebook/test/stubs.ts index 7832b80a6c..6c0f7ed5d7 100644 --- a/src/sql/workbench/contrib/notebook/test/stubs.ts +++ b/src/sql/workbench/contrib/notebook/test/stubs.ts @@ -16,6 +16,7 @@ import { NotebookFindMatch } from 'sql/workbench/contrib/notebook/browser/find/n import { RenderMimeRegistry } from 'sql/workbench/services/notebook/browser/outputs/registry'; import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile'; import { URI } from 'vs/base/common/uri'; +import { QueryTextEditor } from 'sql/workbench/browser/modelComponents/queryTextEditor'; export class NotebookModelStub implements INotebookModel { constructor(private _languageInfo?: nb.ILanguageInfo) { @@ -636,3 +637,63 @@ export class NodeStub implements Node { throw new Error('Method not implemented.'); } } + +export class NotebookEditorStub implements INotebookEditor { + notebookParams: INotebookParams; + id: string; + cells?: ICellModel[]; + cellEditors: CellEditorProviderStub[]; + modelReady: Promise; + model: INotebookModel; + isDirty(): boolean { + throw new Error('Method not implemented.'); + } + isActive(): boolean { + throw new Error('Method not implemented.'); + } + isVisible(): boolean { + throw new Error('Method not implemented.'); + } + executeEdits(edits: ISingleNotebookEditOperation[]): boolean { + throw new Error('Method not implemented.'); + } + runCell(cell: ICellModel): Promise { + throw new Error('Method not implemented.'); + } + runAllCells(startCell?: ICellModel, endCell?: ICellModel): Promise { + throw new Error('Method not implemented.'); + } + clearOutput(cell: ICellModel): Promise { + throw new Error('Method not implemented.'); + } + clearAllOutputs(): Promise { + throw new Error('Method not implemented.'); + } + getSections(): INotebookSection[] { + throw new Error('Method not implemented.'); + } + navigateToSection(sectionId: string): void { + throw new Error('Method not implemented.'); + } + deltaDecorations(newDecorationRange: NotebookRange, oldDecorationRange: NotebookRange): void { + throw new Error('Method not implemented.'); + } + addCell(cellType: CellType, index?: number, event?: UIEvent) { + throw new Error('Method not implemented.'); + } +} + +export class CellEditorProviderStub implements ICellEditorProvider { + hasEditor(): boolean { + throw new Error('Method not implemented.'); + } + cellGuid(): string { + throw new Error('Method not implemented.'); + } + getEditor(): QueryTextEditor { + throw new Error('Method not implemented.'); + } + deltaDecorations(newDecorationRange: NotebookRange, oldDecorationRange: NotebookRange): void { + throw new Error('Method not implemented.'); + } +}