mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-01 01:25:38 -05:00
UI feature - Notebook markdown toolbar (#9853)
* Markdown editor toolbar - initial commit * Moved icons. Refactored new toolbar component to include markup. * Edited markdown toolbar component and referencing in textCell component markup. * Completed UI updates for selected cell toolbar and markdown toolbar. * Modified import path to Event class. Changed EventEmitter to Emitter. * Cleaned up newly added toolbar components * Works sometimes sometimes editor is null * Removed commented out code and styles. CellToolbar and MarkdownToolbar: moved component markup into html file. * Added icon for highlight. Removed more commented code. Re-scoped two styles to their parent components. Corrected templateUrl reference for the new toolbars. * Adjusted paths to SVG icons from toolbar stylesheet. * Add lists and links * Refactor out of component, add actionbar * Support for nothing selected, quick bug fix * Updated split view icons. Added markdown tool backgrounds and cell border colors to color registry and parent components. Updated toolbar icons to use mask as this allows the SVG icon colors to be adjusted on theme change. * Added colorRegistry entries for code cell. Removed colors from styles. Running registerThemingParticipant from code.component. * Revised code component style rules and corrected syntax. * Merged in Chris' working branch and removed unused markup. * Corrected styles and moved another color into colorRegistry for use in new markdown toolbar. * Corrected style error. Overrode left position of content inside textCell and codeCell. Added more entries to colorRegistry. * Moved toolbar and editor icons to common-icons location. Updated related stylesheet. Revised color theming rules for markdown and code cells. * Added themed border between markdown and preview. Moved all notebook themes into notebookStyles.ts * Merged in latest from origin/master and included a small but significant style tweak to light theme code cell toolbar. * Add Undo Support for Markdown Toolbar (#9915) * Remove comment * Renamed registered notebook colors and prefixed with notebook. Moved markdown component theme colors into notebookStyles.ts. Removed colors from cellToolbar styles. Revised icon class names to generic names for better re-use. Removed commented markup. Co-authored-by: chlafreniere <hichise@gmail.com> Co-authored-by: Chris LaFreniere <40371649+chlafreniere@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,344 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
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 { 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';
|
||||
|
||||
|
||||
|
||||
// Action to decorate markdown
|
||||
export class TransformMarkdownAction extends Action {
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string,
|
||||
cssClass: string,
|
||||
tooltip: string,
|
||||
private _cellModel: ICellModel,
|
||||
private _type: MarkdownButtonType,
|
||||
@INotebookService private _notebookService: INotebookService
|
||||
) {
|
||||
super(id, label, cssClass);
|
||||
this._tooltip = tooltip;
|
||||
}
|
||||
public run(context: any): Promise<boolean> {
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
try {
|
||||
let markdownTextTransformer = new MarkdownTextTransformer(this._notebookService, this._cellModel);
|
||||
markdownTextTransformer.transformText(this._type);
|
||||
resolve(true);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class MarkdownTextTransformer {
|
||||
|
||||
private _notebookEditor: INotebookEditor;
|
||||
constructor(private _notebookService: INotebookService, private _cellModel: ICellModel) { }
|
||||
|
||||
public transformText(type: MarkdownButtonType): void {
|
||||
let editorControl = this.getEditorControl();
|
||||
if (editorControl) {
|
||||
let selections = editorControl.getSelections();
|
||||
// TODO: Support replacement for multiple selections
|
||||
let selection = selections[0];
|
||||
let nothingSelected = this.editorHasNoSelection(selection);
|
||||
let startRange: IRange = {
|
||||
startColumn: selection.startColumn,
|
||||
endColumn: selection.startColumn,
|
||||
startLineNumber: selection.startLineNumber,
|
||||
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);
|
||||
|
||||
// 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) {
|
||||
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);
|
||||
} else {
|
||||
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: '' });
|
||||
}
|
||||
editorModel.pushEditOperations(selections, operations, null);
|
||||
}
|
||||
} else {
|
||||
// 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) {
|
||||
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 });
|
||||
}
|
||||
operations.push({ range: endRange, text: endInsertedCode });
|
||||
editorModel.pushEditOperations(selections, operations, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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, 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.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.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;
|
||||
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);
|
||||
}
|
||||
if (this._notebookEditor?.cellEditors?.length > 0) {
|
||||
// Find cell editor provider via cell guid
|
||||
let cellEditorProvider = this._notebookEditor.cellEditors.find(e => e.cellGuid() === this._cellModel.cellGuid);
|
||||
if (cellEditorProvider) {
|
||||
let editor = cellEditorProvider.getEditor() as QueryTextEditor;
|
||||
if (editor) {
|
||||
let editorControl = editor.getControl() as CodeEditorWidget;
|
||||
return editorControl;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private editorHasNoSelection(selection: Selection): boolean {
|
||||
return !selection || (selection.startLineNumber === selection.endLineNumber && selection.startColumn === selection.endColumn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the end selection state after the transform has occurred
|
||||
* @param endRange range for end text that was inserted
|
||||
* @param type MarkdownButtonType
|
||||
* @param editorControl code editor widget
|
||||
* @param noSelection controls whether there was no previous selection in the editor
|
||||
*/
|
||||
private setEndSelection(endRange: IRange, type: MarkdownButtonType, editorControl: CodeEditorWidget, noSelection: boolean, isUndo: boolean): void {
|
||||
if (!endRange || !editorControl || isUndo) {
|
||||
return;
|
||||
}
|
||||
let offset = this.getColumnOffsetForSelection(type, noSelection);
|
||||
if (offset > -1) {
|
||||
let newRange: IRange = {
|
||||
startColumn: endRange.startColumn + offset,
|
||||
startLineNumber: endRange.startLineNumber,
|
||||
endColumn: endRange.startColumn + offset,
|
||||
endLineNumber: endRange.endLineNumber
|
||||
};
|
||||
editorControl.setSelection(newRange);
|
||||
} else {
|
||||
if (this.getMarkdownLineType(type) === MarkdownLineType.BEGIN_AND_END_LINES) {
|
||||
let currentSelection = editorControl.getSelection();
|
||||
editorControl.setSelection({
|
||||
startColumn: currentSelection.startColumn + this.getStartTextToInsert(type).length,
|
||||
startLineNumber: currentSelection.startLineNumber,
|
||||
endColumn: currentSelection.endColumn - this.getEndTextToInsert(type).length,
|
||||
endLineNumber: currentSelection.endLineNumber
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if user wants to perform an undo operation
|
||||
* @param selection current user selection
|
||||
* @param type markdown button type
|
||||
* @param lineType markdown line type
|
||||
* @param editorModel text model for the cell
|
||||
*/
|
||||
private isUndoOperation(selection: Selection, type: MarkdownButtonType, lineType: MarkdownLineType, editorModel: TextModel): boolean {
|
||||
if (lineType === MarkdownLineType.BEGIN_AND_END_LINES) {
|
||||
let selectedText = this.getExtendedSelectedText(selection, type, lineType, editorModel);
|
||||
return selectedText && selectedText.startsWith(this.getStartTextToInsert(type)) && selectedText.endsWith(this.getEndTextToInsert(type));
|
||||
} else {
|
||||
return this.everyLineMatchesBeginString(selection, type, editorModel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the extended selected text (current selection + potential beginning + ending transformed text)
|
||||
* @param selection Current selection in editor
|
||||
* @param type Markdown Button Type
|
||||
* @param lineType Markdown Line Type
|
||||
* @param editorModel TextModel
|
||||
*/
|
||||
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,
|
||||
startLineNumber: selection.startLineNumber,
|
||||
endColumn: selection.endColumn + this.getEndTextToInsert(type).length,
|
||||
endLineNumber: selection.endLineNumber
|
||||
});
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether all lines start with the expected transformed text for actions that match the EVERY_LINE line type
|
||||
* @param selection Current selection in editor
|
||||
* @param type Markdown Button Type
|
||||
* @param editorModel TextModel
|
||||
*/
|
||||
private everyLineMatchesBeginString(selection: Selection, type: MarkdownButtonType, editorModel: TextModel): boolean {
|
||||
if (this.getMarkdownLineType(type) !== MarkdownLineType.EVERY_LINE) {
|
||||
return false;
|
||||
}
|
||||
for (let selectionLine = selection.startLineNumber; selectionLine <= selection.endLineNumber; selectionLine++) {
|
||||
if (!editorModel.getLineContent(selectionLine).startsWith(this.getStartTextToInsert(type))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new IRange object with arbitrary offsets
|
||||
* @param initialRange range object
|
||||
* @param startColumnOffset
|
||||
* @param startLineNumberOffset
|
||||
* @param endColumnOffset
|
||||
* @param endLineNumberOffset
|
||||
*/
|
||||
private getIRangeWithOffsets(initialRange: IRange, startColumnOffset = 0, startLineNumberOffset = 0, endColumnOffset = 0, endLineNumberOffset = 0): IRange {
|
||||
return {
|
||||
startColumn: initialRange.startColumn + startColumnOffset,
|
||||
startLineNumber: initialRange.startLineNumber + startLineNumberOffset,
|
||||
endColumn: initialRange.endColumn + endColumnOffset,
|
||||
endLineNumber: initialRange.endLineNumber + endLineNumberOffset
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export enum MarkdownButtonType {
|
||||
BOLD,
|
||||
ITALIC,
|
||||
CODE,
|
||||
HIGHLIGHT,
|
||||
LINK,
|
||||
UNORDERED_LIST,
|
||||
ORDERED_LIST,
|
||||
IMAGE
|
||||
}
|
||||
|
||||
// If ALL_LINES, we need to insert markdown at each line (e.g. lists)
|
||||
export enum MarkdownLineType {
|
||||
BEGIN_AND_END_LINES,
|
||||
EVERY_LINE
|
||||
}
|
||||
Reference in New Issue
Block a user