mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-19 09:35:36 -05:00
Notebook UI - Markdown toolbar - Headings dropdown (#11049)
* Adds heading dropdown to markdown toolbar. * Added a method specific to headings that places markdown at beginning of line selected. * Rewrote comment for my new method. * Revised code to support multi select for headers, similar to how unordered list is applied. Multi-line headings can be undone if the multi lines are selected. * Modified transformText to make single-line undo operation possible with just the cursor position. * Added utility methods to help determine if the selection is a line-only or multi-line. * Building isReplaceOperation to determine when preceeding characters need to be replaced with a new MarkdownButtonType. * Updated comments. * Applied changes written by Chris. * Reverted changes to earlier stage where heading addition works just like list item additions. * getExtendedSelectedText now returns an actual value in range for MarkdownLineType.EVERY_LINE. * Added conditional so that Preview element is updated only when Preview is enabled. * Updated tests for heading toolbar: heading 1, 2 and 3. * Removed code that could not be reached. * Corrected tests for headings. * wip (cherry picked from commit 43deb9635cc0eeebaffef22d4373f1f6ad713ace) * cleanup * fix error * Fix tests * Add more testing * delete * re-add Co-authored-by: chgagnon <chgagnon@microsoft.com>
This commit is contained in:
@@ -3,12 +3,15 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import 'vs/css!./markdownToolbar';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { Component, Input, Inject, ViewChild, ElementRef } from '@angular/core';
|
||||
import { localize } from 'vs/nls';
|
||||
import { ICellModel } from 'sql/workbench/services/notebook/browser/models/modelInterfaces';
|
||||
import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { TransformMarkdownAction, MarkdownButtonType, TogglePreviewAction } from 'sql/workbench/contrib/notebook/browser/markdownToolbarActions';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { DropdownMenuActionViewItem } from 'sql/base/browser/ui/buttonMenu/buttonMenu';
|
||||
|
||||
export const MARKDOWN_TOOLBAR_SELECTOR: string = 'markdown-toolbar-component';
|
||||
|
||||
@@ -28,12 +31,19 @@ export class MarkdownToolbarComponent {
|
||||
public buttonList = localize('buttonList', "List");
|
||||
public buttonOrderedList = localize('buttonOrderedList', "Ordered list");
|
||||
public buttonImage = localize('buttonImage', "Image");
|
||||
public buttonPreview = localize('buttonPreview', "Markdown preview toggle - off");
|
||||
public dropdownHeading = localize('dropdownHeading', "Heading");
|
||||
public optionHeading1 = localize('optionHeading1', "Heading 1");
|
||||
public optionHeading2 = localize('optionHeading2', "Heading 2");
|
||||
public optionHeading3 = localize('optionHeading3', "Heading 3");
|
||||
public optionParagraph = localize('optionParagraph', "Paragraph");
|
||||
|
||||
@Input() public cellModel: ICellModel;
|
||||
private _actionBar: Taskbar;
|
||||
|
||||
constructor(
|
||||
@Inject(IInstantiationService) private _instantiationService: IInstantiationService
|
||||
@Inject(IInstantiationService) private _instantiationService: IInstantiationService,
|
||||
@Inject(IContextMenuService) private contextMenuService: IContextMenuService
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
@@ -52,9 +62,32 @@ export class MarkdownToolbarComponent {
|
||||
let imageButton = this._instantiationService.createInstance(TransformMarkdownAction, 'notebook.imageText', '', 'insert-image', this.buttonImage, this.cellModel, MarkdownButtonType.IMAGE);
|
||||
let togglePreviewAction = this._instantiationService.createInstance(TogglePreviewAction, 'notebook.togglePreview', true, this.cellModel.showPreview);
|
||||
|
||||
let headingDropdown = this._instantiationService.createInstance(TransformMarkdownAction, 'notebook.heading', '', 'heading', this.dropdownHeading, this.cellModel, null);
|
||||
let heading1 = this._instantiationService.createInstance(TransformMarkdownAction, 'notebook.heading1', this.optionHeading1, 'heading 1', this.optionHeading1, this.cellModel, MarkdownButtonType.HEADING1);
|
||||
let heading2 = this._instantiationService.createInstance(TransformMarkdownAction, 'notebook.heading2', this.optionHeading2, 'heading 2', this.optionHeading2, this.cellModel, MarkdownButtonType.HEADING2);
|
||||
let heading3 = this._instantiationService.createInstance(TransformMarkdownAction, 'notebook.heading3', this.optionHeading3, 'heading 3', this.optionHeading3, this.cellModel, MarkdownButtonType.HEADING3);
|
||||
let paragraph = this._instantiationService.createInstance(TransformMarkdownAction, 'notebook.paragraph', this.optionParagraph, 'paragraph', this.optionParagraph, this.cellModel, MarkdownButtonType.PARAGRAPH);
|
||||
|
||||
let taskbar = <HTMLElement>this.mdtoolbar.nativeElement;
|
||||
this._actionBar = new Taskbar(taskbar);
|
||||
this._actionBar.context = this;
|
||||
|
||||
let buttonDropdownContainer = DOM.$('li.action-item');
|
||||
buttonDropdownContainer.setAttribute('role', 'presentation');
|
||||
let dropdownMenuActionViewItem = new DropdownMenuActionViewItem(
|
||||
headingDropdown,
|
||||
[heading1, heading2, heading3, paragraph],
|
||||
this.contextMenuService,
|
||||
undefined,
|
||||
this._actionBar.actionRunner,
|
||||
undefined,
|
||||
'notebook-button masked-pseudo-after dropdown-arrow',
|
||||
this.optionParagraph,
|
||||
undefined
|
||||
);
|
||||
dropdownMenuActionViewItem.render(buttonDropdownContainer);
|
||||
dropdownMenuActionViewItem.setActionContext(this);
|
||||
|
||||
this._actionBar.setContent([
|
||||
{ action: boldButton },
|
||||
{ action: italicButton },
|
||||
@@ -65,6 +98,7 @@ export class MarkdownToolbarComponent {
|
||||
{ action: listButton },
|
||||
{ action: orderedListButton },
|
||||
{ action: imageButton },
|
||||
{ element: buttonDropdownContainer },
|
||||
{ action: togglePreviewAction }
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -34,13 +34,26 @@
|
||||
.markdown-toolbar .carbon-taskbar li:nth-child(2) {
|
||||
margin-right: 9px;
|
||||
}
|
||||
.markdown-toolbar .carbon-taskbar li.action-item .masked-pseudo-after.dropdown-arrow {
|
||||
background-color: transparent;
|
||||
font-size: 14px;
|
||||
height: 100%;
|
||||
line-height: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
.markdown-toolbar .carbon-taskbar li.action-item .masked-pseudo-after.dropdown-arrow:after {
|
||||
position: relative;
|
||||
right: -6px;
|
||||
top: -3px;
|
||||
width: 26px;
|
||||
}
|
||||
.markdown-toolbar .carbon-taskbar li:last-child {
|
||||
margin-right: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
.markdown-toolbar .carbon-taskbar li a {
|
||||
display: inline-block;
|
||||
display: flex;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
-webkit-mask-position: center;
|
||||
|
||||
@@ -199,9 +199,11 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges {
|
||||
});
|
||||
this.markdownResult.element.innerHTML = this.sanitizeContent(this.markdownResult.element.innerHTML);
|
||||
this.setLoading(false);
|
||||
let outputElement = <HTMLElement>this.output.nativeElement;
|
||||
outputElement.innerHTML = this.markdownResult.element.innerHTML;
|
||||
this.cellModel.renderedOutputTextContent = this.getRenderedTextOutput();
|
||||
if (this.showPreview) {
|
||||
let outputElement = <HTMLElement>this.output.nativeElement;
|
||||
outputElement.innerHTML = this.markdownResult.element.innerHTML;
|
||||
this.cellModel.renderedOutputTextContent = this.getRenderedTextOutput();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,16 +7,17 @@ import { localize } from 'vs/nls';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { INotebookEditor, INotebookService } from 'sql/workbench/services/notebook/browser/notebookService';
|
||||
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget';
|
||||
import { IRange } from 'vs/editor/common/core/range';
|
||||
import { IRange, Range } from 'vs/editor/common/core/range';
|
||||
import { IIdentifiedSingleEditOperation } from 'vs/editor/common/model';
|
||||
import { TextModel } from 'vs/editor/common/model/textModel';
|
||||
import { ICellModel } from 'sql/workbench/services/notebook/browser/models/modelInterfaces';
|
||||
import { QueryTextEditor } from 'sql/workbench/browser/modelComponents/queryTextEditor';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { ToggleableAction } from 'sql/workbench/contrib/notebook/browser/notebookActions';
|
||||
import { EditOperation } from 'vs/editor/common/core/editOperation';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
|
||||
|
||||
// Action to decorate markdown
|
||||
export class TransformMarkdownAction extends Action {
|
||||
|
||||
constructor(
|
||||
@@ -66,128 +67,39 @@ export class MarkdownTextTransformer {
|
||||
endLineNumber: selection.startLineNumber
|
||||
};
|
||||
|
||||
// Get text to insert before selection
|
||||
let beginInsertedCode = this.getStartTextToInsert(type);
|
||||
// Get text to insert after selection
|
||||
let endInsertedCode = this.getEndTextToInsert(type);
|
||||
let beginInsertedText = getStartTextToInsert(type);
|
||||
let endInsertedText = getEndTextToInsert(type);
|
||||
|
||||
// endInsertedCode can be an empty string (e.g. for unordered list), so no need to check for that as well
|
||||
if (beginInsertedCode) {
|
||||
let endRange: IRange = {
|
||||
startColumn: selection.endColumn,
|
||||
endColumn: selection.endColumn,
|
||||
startLineNumber: selection.endLineNumber,
|
||||
endLineNumber: selection.endLineNumber
|
||||
};
|
||||
let editorModel = editorControl.getModel() as TextModel;
|
||||
let isUndo = false;
|
||||
if (editorModel) {
|
||||
let markdownLineType = this.getMarkdownLineType(type);
|
||||
isUndo = this.isUndoOperation(selection, type, markdownLineType, editorModel);
|
||||
if (isUndo) {
|
||||
this.handleUndoOperation(markdownLineType, startRange, endRange, editorModel, beginInsertedCode, endInsertedCode, selections, selection);
|
||||
} else {
|
||||
this.handleTransformOperation(markdownLineType, startRange, endRange, editorModel, beginInsertedCode, endInsertedCode, selections, selection);
|
||||
}
|
||||
let endRange: IRange = {
|
||||
startColumn: selection.endColumn,
|
||||
endColumn: selection.endColumn,
|
||||
startLineNumber: selection.endLineNumber,
|
||||
endLineNumber: selection.endLineNumber
|
||||
};
|
||||
|
||||
let editorModel = editorControl.getModel() as TextModel;
|
||||
let isUndo = false;
|
||||
if (editorModel) {
|
||||
let markdownLineType = getMarkdownLineType(type);
|
||||
// Paragraph (empty string) is just used for replacing any existing headers so will never be an undo operation
|
||||
isUndo = beginInsertedText && this.isUndoOperation(selection, type, markdownLineType, editorModel);
|
||||
if (isUndo) {
|
||||
this.handleUndoOperation(markdownLineType, startRange, endRange, editorModel, beginInsertedText, endInsertedText, selections, selection);
|
||||
} else {
|
||||
this.handleTransformOperation(markdownLineType, type, startRange, endRange, editorModel, beginInsertedText, endInsertedText, selections, selection);
|
||||
}
|
||||
|
||||
// If selection end is on same line as beginning, need to add offset for number of characters inserted
|
||||
// Otherwise, the selection will not be correct after the transformation
|
||||
let offset = selection.startLineNumber === selection.endLineNumber ? beginInsertedCode.length : 0;
|
||||
endRange = this.getIRangeWithOffsets(endRange, offset, 0, offset, 0);
|
||||
this.setEndSelection(endRange, type, editorControl, editorModel, nothingSelected, isUndo);
|
||||
}
|
||||
|
||||
// If selection end is on same line as beginning, need to add offset for number of characters inserted
|
||||
// Otherwise, the selection will not be correct after the transformation
|
||||
let offset = selection.startLineNumber === selection.endLineNumber ? beginInsertedText.length : 0;
|
||||
endRange = this.getIRangeWithOffsets(endRange, offset, 0, offset, 0);
|
||||
this.setEndSelection(endRange, type, editorControl, editorModel, nothingSelected, isUndo);
|
||||
// Always give focus back to the editor after pressing the button
|
||||
editorControl.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// For items like lists (where we need to insert a character at the beginning of each line), create
|
||||
// range object for that range
|
||||
private transformRangeByLineOffset(range: IRange, lineOffset: number): IRange {
|
||||
return {
|
||||
startColumn: lineOffset === 0 ? range.startColumn : 1,
|
||||
endColumn: range.endColumn,
|
||||
startLineNumber: range.endLineNumber + lineOffset,
|
||||
endLineNumber: range.endLineNumber + lineOffset
|
||||
};
|
||||
}
|
||||
|
||||
private getStartTextToInsert(type: MarkdownButtonType): string {
|
||||
switch (type) {
|
||||
case MarkdownButtonType.BOLD:
|
||||
return '**';
|
||||
case MarkdownButtonType.ITALIC:
|
||||
return '_';
|
||||
case MarkdownButtonType.UNDERLINE:
|
||||
return '<u>';
|
||||
case MarkdownButtonType.CODE:
|
||||
return '```\n';
|
||||
case MarkdownButtonType.LINK:
|
||||
return '[';
|
||||
case MarkdownButtonType.UNORDERED_LIST:
|
||||
return '- ';
|
||||
case MarkdownButtonType.ORDERED_LIST:
|
||||
return '1. ';
|
||||
case MarkdownButtonType.IMAGE:
|
||||
return '![';
|
||||
case MarkdownButtonType.HIGHLIGHT:
|
||||
return '<mark>';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
private getEndTextToInsert(type: MarkdownButtonType): string {
|
||||
switch (type) {
|
||||
case MarkdownButtonType.BOLD:
|
||||
return '**';
|
||||
case MarkdownButtonType.ITALIC:
|
||||
return '_';
|
||||
case MarkdownButtonType.UNDERLINE:
|
||||
return '</u>';
|
||||
case MarkdownButtonType.CODE:
|
||||
return '\n```';
|
||||
case MarkdownButtonType.LINK:
|
||||
case MarkdownButtonType.IMAGE:
|
||||
return ']()';
|
||||
case MarkdownButtonType.HIGHLIGHT:
|
||||
return '</mark>';
|
||||
case MarkdownButtonType.UNORDERED_LIST:
|
||||
case MarkdownButtonType.ORDERED_LIST:
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
private getMarkdownLineType(type: MarkdownButtonType): MarkdownLineType {
|
||||
switch (type) {
|
||||
case MarkdownButtonType.UNORDERED_LIST:
|
||||
case MarkdownButtonType.ORDERED_LIST:
|
||||
return MarkdownLineType.EVERY_LINE;
|
||||
case MarkdownButtonType.CODE:
|
||||
return MarkdownLineType.WRAPPED_ABOVE_AND_BELOW;
|
||||
default:
|
||||
return MarkdownLineType.BEGIN_AND_END_LINES;
|
||||
}
|
||||
}
|
||||
|
||||
// Get offset from the end column for editor selection
|
||||
// For example, when inserting a link, we want to have the cursor be present in between the brackets
|
||||
private getColumnOffsetForSelection(type: MarkdownButtonType, nothingSelected: boolean): number {
|
||||
if (nothingSelected) {
|
||||
return 0;
|
||||
}
|
||||
switch (type) {
|
||||
case MarkdownButtonType.LINK:
|
||||
return 2;
|
||||
case MarkdownButtonType.IMAGE:
|
||||
return 2;
|
||||
// -1 is considered as having no explicit offset, so do not do anything with selection
|
||||
default: return -1;
|
||||
}
|
||||
}
|
||||
|
||||
private getEditorControl(): CodeEditorWidget | undefined {
|
||||
if (!this._notebookEditor) {
|
||||
this._notebookEditor = this._notebookService.findNotebookEditor(this._cellModel?.notebookModel?.notebookUri);
|
||||
@@ -221,7 +133,7 @@ export class MarkdownTextTransformer {
|
||||
if (!endRange || !editorControl || isUndo) {
|
||||
return;
|
||||
}
|
||||
let offset = this.getColumnOffsetForSelection(type, noSelection);
|
||||
let offset = getColumnOffsetForSelection(type, noSelection);
|
||||
if (offset > -1) {
|
||||
let newRange: IRange;
|
||||
if (type !== MarkdownButtonType.CODE) {
|
||||
@@ -241,13 +153,13 @@ export class MarkdownTextTransformer {
|
||||
}
|
||||
editorControl.setSelection(newRange);
|
||||
} else {
|
||||
let markdownLineType = this.getMarkdownLineType(type);
|
||||
let markdownLineType = getMarkdownLineType(type);
|
||||
let currentSelection = editorControl.getSelection();
|
||||
if (markdownLineType === MarkdownLineType.BEGIN_AND_END_LINES) {
|
||||
editorControl.setSelection({
|
||||
startColumn: currentSelection.startColumn + this.getStartTextToInsert(type).length,
|
||||
startColumn: currentSelection.startColumn + getStartTextToInsert(type).length,
|
||||
startLineNumber: currentSelection.startLineNumber,
|
||||
endColumn: currentSelection.endColumn - this.getEndTextToInsert(type).length,
|
||||
endColumn: currentSelection.endColumn - getEndTextToInsert(type).length,
|
||||
endLineNumber: currentSelection.endLineNumber
|
||||
});
|
||||
} else if (markdownLineType === MarkdownLineType.WRAPPED_ABOVE_AND_BELOW) {
|
||||
@@ -272,8 +184,8 @@ export class MarkdownTextTransformer {
|
||||
*/
|
||||
private isUndoOperation(selection: Selection, type: MarkdownButtonType, lineType: MarkdownLineType, editorModel: TextModel): boolean {
|
||||
if (lineType === MarkdownLineType.BEGIN_AND_END_LINES || lineType === MarkdownLineType.WRAPPED_ABOVE_AND_BELOW) {
|
||||
let selectedText = this.getExtendedSelectedText(selection, type, lineType, editorModel);
|
||||
return selectedText && selectedText.startsWith(this.getStartTextToInsert(type)) && selectedText.endsWith(this.getEndTextToInsert(type));
|
||||
const selectedText = this.getExtendedSelectedText(selection, type, lineType, editorModel);
|
||||
return selectedText && selectedText.startsWith(getStartTextToInsert(type)) && selectedText.endsWith(getEndTextToInsert(type));
|
||||
} else {
|
||||
return this.everyLineMatchesBeginString(selection, type, editorModel);
|
||||
}
|
||||
@@ -283,32 +195,53 @@ export class MarkdownTextTransformer {
|
||||
if (markdownLineType === MarkdownLineType.BEGIN_AND_END_LINES) {
|
||||
startRange = this.getIRangeWithOffsets(startRange, -1 * beginInsertedCode.length, 0, 0, 0);
|
||||
endRange = this.getIRangeWithOffsets(endRange, 0, 0, endInsertedCode.length, 0);
|
||||
editorModel.pushEditOperations(selections, [{ range: endRange, text: '' }, { range: startRange, text: '' }], null);
|
||||
editorModel.pushEditOperations(selections, [{ range: endRange, text: '' }, { range: startRange, text: '' }], undefined);
|
||||
} else if (markdownLineType === MarkdownLineType.WRAPPED_ABOVE_AND_BELOW) {
|
||||
startRange = this.getIRangeWithOffsets(startRange, 0, -1, 0, 0);
|
||||
endRange = this.getIRangeWithOffsets(endRange, 0, 0, endInsertedCode.length, 1);
|
||||
editorModel.pushEditOperations(selections, [{ range: endRange, text: '' }, { range: startRange, text: '' }], null);
|
||||
} else {
|
||||
// Delete the entire rows above and below the current selection
|
||||
const startLineRange = new Range(startRange.startLineNumber - 1, 1, startRange.startLineNumber, 1);
|
||||
const endLineRange = new Range(endRange.startLineNumber, editorModel.getLineLength(endRange.startLineNumber) + 1, endRange.startLineNumber + 1, endInsertedCode.length + 1);
|
||||
editorModel.pushEditOperations(selections, [EditOperation.delete(endLineRange), EditOperation.delete(startLineRange)], undefined);
|
||||
} else if (markdownLineType === MarkdownLineType.EVERY_LINE) {
|
||||
let operations: IIdentifiedSingleEditOperation[] = [];
|
||||
startRange = this.getIRangeWithOffsets(startRange, 0, 0, beginInsertedCode.length, 0);
|
||||
for (let i = 0; i < selection.endLineNumber - selection.startLineNumber + 1; i++) {
|
||||
operations.push({ range: this.transformRangeByLineOffset(startRange, i), text: '' });
|
||||
for (let i = selection.startLineNumber; i <= selection.endLineNumber; i++) {
|
||||
// If we're in an undo operation we already verified that every line starts with the expected text
|
||||
// Create the edit operation to delete the text for every line
|
||||
operations.push(EditOperation.delete(new Range(i, 1, i, beginInsertedCode.length + 1)));
|
||||
}
|
||||
editorModel.pushEditOperations(selections, operations, null);
|
||||
editorModel.pushEditOperations(selections, operations, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
handleTransformOperation(markdownLineType: MarkdownLineType, startRange: IRange, endRange: IRange, editorModel: TextModel, beginInsertedCode: string, endInsertedCode: string, selections: Selection[], selection: Selection): void {
|
||||
private handleTransformOperation(markdownLineType: MarkdownLineType, markdownButtonType: MarkdownButtonType, startRange: IRange, endRange: IRange, editorModel: TextModel, beginInsertedCode: string, endInsertedCode: string, selections: Selection[], selection: Selection): void {
|
||||
// If the markdown we're inserting only needs to be added to the begin and end lines, add those edit operations directly
|
||||
if (markdownLineType === MarkdownLineType.BEGIN_AND_END_LINES || markdownLineType === MarkdownLineType.WRAPPED_ABOVE_AND_BELOW) {
|
||||
editorModel.pushEditOperations(selections, [{ range: startRange, text: beginInsertedCode }, { range: endRange, text: endInsertedCode }], null);
|
||||
} else { // Otherwise, add an operation per line (plus the operation at the last column + line)
|
||||
let operations: IIdentifiedSingleEditOperation[] = [];
|
||||
for (let i = 0; i < selection.endLineNumber - selection.startLineNumber + 1; i++) {
|
||||
operations.push({ range: this.transformRangeByLineOffset(startRange, i), text: beginInsertedCode });
|
||||
editorModel.pushEditOperations(selections, [
|
||||
{ range: startRange, text: beginInsertedCode },
|
||||
{ range: endRange, text: endInsertedCode }], undefined);
|
||||
} else if (markdownLineType === MarkdownLineType.EVERY_LINE) {
|
||||
const replacementTokens = getStartTextToReplace(markdownButtonType);
|
||||
const operations: IIdentifiedSingleEditOperation[] = [];
|
||||
// Create the edit operation to insert the text for every line
|
||||
for (let i = selection.startLineNumber; i <= selection.endLineNumber; i++) {
|
||||
// If this token is part of a group then see if the text for this line
|
||||
// starts with any of the tokens in that group
|
||||
const replacementText = replacementTokens.find(t => {
|
||||
const text = editorModel.getValueInRange({
|
||||
startColumn: 1,
|
||||
startLineNumber: i,
|
||||
endColumn: t.length + 1,
|
||||
endLineNumber: i
|
||||
});
|
||||
return text === t;
|
||||
});
|
||||
// If we have text to replace do that - otherwise just insert the text directly
|
||||
if (replacementText) {
|
||||
operations.push(EditOperation.replace(new Range(i, 1, i, replacementText.length + 1), beginInsertedCode));
|
||||
} else {
|
||||
operations.push(EditOperation.insert(new Position(i, 0), beginInsertedCode));
|
||||
}
|
||||
}
|
||||
operations.push({ range: endRange, text: endInsertedCode });
|
||||
editorModel.pushEditOperations(selections, operations, null);
|
||||
editorModel.pushEditOperations(selections, operations, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,16 +255,16 @@ export class MarkdownTextTransformer {
|
||||
private getExtendedSelectedText(selection: Selection, type: MarkdownButtonType, lineType: MarkdownLineType, editorModel: TextModel): string {
|
||||
if (lineType === MarkdownLineType.BEGIN_AND_END_LINES) {
|
||||
return editorModel.getValueInRange({
|
||||
startColumn: selection.startColumn - this.getStartTextToInsert(type).length,
|
||||
startColumn: selection.startColumn - getStartTextToInsert(type).length,
|
||||
startLineNumber: selection.startLineNumber,
|
||||
endColumn: selection.endColumn + this.getEndTextToInsert(type).length,
|
||||
endColumn: selection.endColumn + getEndTextToInsert(type).length,
|
||||
endLineNumber: selection.endLineNumber
|
||||
});
|
||||
} else if (lineType === MarkdownLineType.WRAPPED_ABOVE_AND_BELOW) {
|
||||
return editorModel.getValueInRange({
|
||||
startColumn: 1,
|
||||
startLineNumber: selection.startLineNumber - 1,
|
||||
endColumn: this.getEndTextToInsert(type).length + 1,
|
||||
endColumn: getEndTextToInsert(type).length + 1,
|
||||
endLineNumber: selection.endLineNumber + 1
|
||||
});
|
||||
}
|
||||
@@ -346,7 +279,7 @@ export class MarkdownTextTransformer {
|
||||
*/
|
||||
private everyLineMatchesBeginString(selection: Selection, type: MarkdownButtonType, editorModel: TextModel): boolean {
|
||||
for (let selectionLine = selection.startLineNumber; selectionLine <= selection.endLineNumber; selectionLine++) {
|
||||
if (!editorModel.getLineContent(selectionLine).startsWith(this.getStartTextToInsert(type))) {
|
||||
if (!editorModel.getLineContent(selectionLine).startsWith(getStartTextToInsert(type))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -380,17 +313,166 @@ export enum MarkdownButtonType {
|
||||
LINK,
|
||||
UNORDERED_LIST,
|
||||
ORDERED_LIST,
|
||||
IMAGE
|
||||
IMAGE,
|
||||
HEADING1,
|
||||
HEADING2,
|
||||
HEADING3,
|
||||
PARAGRAPH
|
||||
}
|
||||
|
||||
// If ALL_LINES, we need to insert markdown at each line (e.g. lists)
|
||||
// WRAPPED_ABOVE_AND_BELOW puts text above and below the highlighted text
|
||||
/**
|
||||
* The line types
|
||||
*/
|
||||
export enum MarkdownLineType {
|
||||
/**
|
||||
* Applies to the beginning and end lines only of a selection
|
||||
*/
|
||||
BEGIN_AND_END_LINES,
|
||||
/**
|
||||
* Applies to every line within the selection
|
||||
*/
|
||||
EVERY_LINE,
|
||||
/**
|
||||
* Applies to the entire selection by wrapping it in new text above and below
|
||||
*/
|
||||
WRAPPED_ABOVE_AND_BELOW
|
||||
}
|
||||
|
||||
/**
|
||||
* Groups of related types - these will be considered as the same when doing a transformation
|
||||
* so will replace each other instead of just prepending new text if the line already starts
|
||||
* with the text for another member of the group.
|
||||
*/
|
||||
const buttonTypeGroups = [
|
||||
[
|
||||
MarkdownButtonType.HEADING1,
|
||||
MarkdownButtonType.HEADING2,
|
||||
MarkdownButtonType.HEADING3,
|
||||
MarkdownButtonType.PARAGRAPH
|
||||
]
|
||||
];
|
||||
|
||||
/**
|
||||
* Gets the list of strings that will be replaced if a selection starts
|
||||
* with the specified text
|
||||
* @param type The button type action being ran
|
||||
*/
|
||||
function getStartTextToReplace(type: MarkdownButtonType): string[] {
|
||||
for (const group of buttonTypeGroups) {
|
||||
const item = group.find(value => value === type);
|
||||
if (item) {
|
||||
return group.filter(item => item !== type).map(item => getStartTextToInsert(item));
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the text to insert at the beginning of the selection
|
||||
* @param type The button type action being ran
|
||||
*/
|
||||
function getStartTextToInsert(type: MarkdownButtonType): string {
|
||||
switch (type) {
|
||||
case MarkdownButtonType.BOLD:
|
||||
return '**';
|
||||
case MarkdownButtonType.ITALIC:
|
||||
return '_';
|
||||
case MarkdownButtonType.UNDERLINE:
|
||||
return '<u>';
|
||||
case MarkdownButtonType.CODE:
|
||||
return '```\n';
|
||||
case MarkdownButtonType.LINK:
|
||||
return '[';
|
||||
case MarkdownButtonType.UNORDERED_LIST:
|
||||
return '- ';
|
||||
case MarkdownButtonType.ORDERED_LIST:
|
||||
return '1. ';
|
||||
case MarkdownButtonType.IMAGE:
|
||||
return '![';
|
||||
case MarkdownButtonType.HIGHLIGHT:
|
||||
return '<mark>';
|
||||
case MarkdownButtonType.HEADING1:
|
||||
return '# ';
|
||||
case MarkdownButtonType.HEADING2:
|
||||
return '## ';
|
||||
case MarkdownButtonType.HEADING3:
|
||||
return '### ';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the text to insert at the end of the selection
|
||||
* @param type The button type action being ran
|
||||
*/
|
||||
function getEndTextToInsert(type: MarkdownButtonType): string {
|
||||
switch (type) {
|
||||
case MarkdownButtonType.BOLD:
|
||||
return '**';
|
||||
case MarkdownButtonType.ITALIC:
|
||||
return '_';
|
||||
case MarkdownButtonType.UNDERLINE:
|
||||
return '</u>';
|
||||
case MarkdownButtonType.CODE:
|
||||
return '\n```';
|
||||
case MarkdownButtonType.LINK:
|
||||
case MarkdownButtonType.IMAGE:
|
||||
return ']()';
|
||||
case MarkdownButtonType.HIGHLIGHT:
|
||||
return '</mark>';
|
||||
case MarkdownButtonType.UNORDERED_LIST:
|
||||
case MarkdownButtonType.ORDERED_LIST:
|
||||
case MarkdownButtonType.HEADING1:
|
||||
case MarkdownButtonType.HEADING2:
|
||||
case MarkdownButtonType.HEADING3:
|
||||
case MarkdownButtonType.PARAGRAPH:
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the line type that a button type applies to
|
||||
* @param type The button type action being ran
|
||||
*/
|
||||
function getMarkdownLineType(type: MarkdownButtonType): MarkdownLineType {
|
||||
switch (type) {
|
||||
case MarkdownButtonType.CODE:
|
||||
return MarkdownLineType.WRAPPED_ABOVE_AND_BELOW;
|
||||
case MarkdownButtonType.UNORDERED_LIST:
|
||||
case MarkdownButtonType.ORDERED_LIST:
|
||||
case MarkdownButtonType.HEADING1:
|
||||
case MarkdownButtonType.HEADING2:
|
||||
case MarkdownButtonType.HEADING3:
|
||||
case MarkdownButtonType.PARAGRAPH:
|
||||
return MarkdownLineType.EVERY_LINE;
|
||||
default:
|
||||
return MarkdownLineType.BEGIN_AND_END_LINES;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get offset from the end column for editor selection
|
||||
* For example, when inserting a link, we want to have the cursor be present in between the brackets
|
||||
* @param type
|
||||
* @param nothingSelected
|
||||
*/
|
||||
function getColumnOffsetForSelection(type: MarkdownButtonType, nothingSelected: boolean): number {
|
||||
if (nothingSelected) {
|
||||
return 0;
|
||||
}
|
||||
switch (type) {
|
||||
case MarkdownButtonType.LINK:
|
||||
return 2;
|
||||
case MarkdownButtonType.IMAGE:
|
||||
return 2;
|
||||
// -1 is considered as having no explicit offset, so do not do anything with selection
|
||||
default: return -1;
|
||||
}
|
||||
}
|
||||
|
||||
export class TogglePreviewAction extends ToggleableAction {
|
||||
|
||||
private static readonly previewShowLabel = localize('previewShowLabel', "Show Preview");
|
||||
|
||||
@@ -30,8 +30,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment'
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { IEditor } from 'vs/editor/common/editorCommon';
|
||||
import { NotebookEditorStub } from 'sql/workbench/contrib/notebook/test/testCommon';
|
||||
|
||||
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
|
||||
suite('MarkdownTextTransformer', () => {
|
||||
let markdownTextTransformer: MarkdownTextTransformer;
|
||||
@@ -74,7 +73,6 @@ suite('MarkdownTextTransformer', () => {
|
||||
// Couple widget with newly created text model
|
||||
widget.setModel(textModel);
|
||||
|
||||
// let textModel = widget.getModel() as TextModel;
|
||||
assert(!isUndefinedOrNull(widget.getModel()), 'Text model is undefined');
|
||||
});
|
||||
|
||||
@@ -90,22 +88,21 @@ suite('MarkdownTextTransformer', () => {
|
||||
testWithNoSelection(MarkdownButtonType.LINK, '[]()', true);
|
||||
testWithNoSelection(MarkdownButtonType.LINK, '');
|
||||
testWithNoSelection(MarkdownButtonType.UNORDERED_LIST, '- ', true);
|
||||
testWithNoSelection(MarkdownButtonType.UNORDERED_LIST, '- ');
|
||||
testWithNoSelection(MarkdownButtonType.UNORDERED_LIST, '');
|
||||
testWithNoSelection(MarkdownButtonType.ORDERED_LIST, '1. ', true);
|
||||
testWithNoSelection(MarkdownButtonType.ORDERED_LIST, '1. ');
|
||||
testWithNoSelection(MarkdownButtonType.ORDERED_LIST, '');
|
||||
testWithNoSelection(MarkdownButtonType.IMAGE, '![]()', true);
|
||||
testWithNoSelection(MarkdownButtonType.IMAGE, '');
|
||||
testWithNoSelection(MarkdownButtonType.HEADING1, '# ', true);
|
||||
testWithNoSelection(MarkdownButtonType.HEADING1, '');
|
||||
testWithNoSelection(MarkdownButtonType.HEADING2, '## ', true);
|
||||
testWithNoSelection(MarkdownButtonType.HEADING2, '');
|
||||
testWithNoSelection(MarkdownButtonType.HEADING3, '### ', true);
|
||||
testWithNoSelection(MarkdownButtonType.HEADING3, '');
|
||||
});
|
||||
|
||||
test('Transform text with one word selected', () => {
|
||||
testWithSingleWordSelected(MarkdownButtonType.BOLD, '**WORD**');
|
||||
testWithSingleWordSelected(MarkdownButtonType.ITALIC, '_WORD_');
|
||||
testWithSingleWordSelected(MarkdownButtonType.CODE, '```\nWORD\n```');
|
||||
testWithSingleWordSelected(MarkdownButtonType.HIGHLIGHT, '<mark>WORD</mark>');
|
||||
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', () => {
|
||||
@@ -128,6 +125,9 @@ suite('MarkdownTextTransformer', () => {
|
||||
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]()');
|
||||
testWithMultipleLinesSelected(MarkdownButtonType.HEADING1, '# Multi\n# Lines\n# Selected');
|
||||
testWithMultipleLinesSelected(MarkdownButtonType.HEADING2, '## Multi\n## Lines\n## Selected');
|
||||
testWithMultipleLinesSelected(MarkdownButtonType.HEADING3, '### Multi\n### Lines\n### Selected');
|
||||
});
|
||||
|
||||
test('Ensure notebook editor returns expected object', () => {
|
||||
@@ -146,16 +146,26 @@ suite('MarkdownTextTransformer', () => {
|
||||
textModel.setValue('');
|
||||
}
|
||||
markdownTextTransformer.transformText(type);
|
||||
assert.equal(textModel.getValue(), expectedValue, `${MarkdownButtonType[type]} with no selection failed`);
|
||||
assert.equal(textModel.getValue(), expectedValue, `${MarkdownButtonType[type]} with no selection failed (setValue ${setValue})`);
|
||||
}
|
||||
|
||||
function testWithSingleWordSelected(type: MarkdownButtonType, expectedValue: string): void {
|
||||
let value = 'WORD';
|
||||
textModel.setValue(value);
|
||||
widget.setSelection({ startColumn: 1, startLineNumber: 1, endColumn: 5, endLineNumber: 1 });
|
||||
|
||||
// Test transformation (adding text)
|
||||
widget.setSelection({ startColumn: 1, startLineNumber: 1, endColumn: value.length + 1, 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`);
|
||||
const textModelValue = textModel.getValue();
|
||||
assert.equal(textModelValue, expectedValue, `${MarkdownButtonType[type]} with single word selection failed`);
|
||||
|
||||
// Test undo (removing text)
|
||||
const valueRange = getValueRange(textModel, value);
|
||||
assert.notEqual(valueRange, undefined, 'Could not find value in model after transformation');
|
||||
widget.setSelection(valueRange);
|
||||
markdownTextTransformer.transformText(type);
|
||||
assert.equal(textModel.getValue(), value, `Undo operation for ${MarkdownButtonType[type]} with single word selection failed`);
|
||||
}
|
||||
|
||||
function testWithMultipleWordsSelected(type: MarkdownButtonType, expectedValue: string): void {
|
||||
@@ -165,6 +175,13 @@ suite('MarkdownTextTransformer', () => {
|
||||
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`);
|
||||
|
||||
// Test undo (removing text)
|
||||
const valueRange = getValueRange(textModel, value);
|
||||
assert.notEqual(valueRange, undefined, 'Could not find value in model after transformation');
|
||||
widget.setSelection(valueRange);
|
||||
markdownTextTransformer.transformText(type);
|
||||
assert.equal(textModel.getValue(), value, `Undo operation for ${MarkdownButtonType[type]} with multiple word selection failed`);
|
||||
}
|
||||
|
||||
function testWithMultipleLinesSelected(type: MarkdownButtonType, expectedValue: string): void {
|
||||
@@ -174,6 +191,32 @@ suite('MarkdownTextTransformer', () => {
|
||||
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`);
|
||||
|
||||
// Test undo (removing text)
|
||||
let valueRange = getValueRange(textModel, 'Multi');
|
||||
// Modify the range to include all the lines
|
||||
valueRange = new Range(valueRange.startLineNumber, valueRange.startColumn, valueRange.endLineNumber + 2, 9);
|
||||
assert.notEqual(valueRange, undefined, 'Could not find value in model after transformation');
|
||||
widget.setSelection(valueRange);
|
||||
markdownTextTransformer.transformText(type);
|
||||
assert.equal(textModel.getValue(), value, `Undo operation for ${MarkdownButtonType[type]} with multiple line selection failed`);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Searches the model for the specified string value and if found returns a range for the last
|
||||
* occurence of that value.
|
||||
* @param textModel The model to search
|
||||
* @param value The value to search for
|
||||
*/
|
||||
function getValueRange(textModel: TextModel, value: string): Range | undefined {
|
||||
const linesContent = textModel.getLinesContent();
|
||||
let range = undefined;
|
||||
linesContent.forEach((line, index) => {
|
||||
const valueIndex = line.indexOf(value);
|
||||
if (valueIndex >= 0) {
|
||||
range = new Range(index + 1, valueIndex + 1, index + 1, valueIndex + value.length + 1);
|
||||
}
|
||||
});
|
||||
return range;
|
||||
}
|
||||
Reference in New Issue
Block a user