From cef3c064203d8766d677482cedbfabac8281eebb Mon Sep 17 00:00:00 2001 From: rebornix Date: Tue, 25 Feb 2020 15:17:38 -0800 Subject: [PATCH] find widget attempt --- src/vs/editor/common/model.ts | 6 ++ src/vs/editor/common/model/textModel.ts | 5 + src/vs/editor/contrib/find/findController.ts | 5 + .../api/browser/mainThreadNotebook.ts | 14 +++ .../notebook/browser/notebook.contribution.ts | 3 +- .../notebook/browser/notebookActions.ts | 102 +++++++++++++----- .../notebook/browser/notebookBrowser.ts | 3 + .../notebook/browser/notebookEditor.ts | 74 +++++++++---- .../notebook/browser/notebookEditorInput.ts | 5 + .../notebook/browser/notebookFindWidget.ts | 92 ++++++++++++++++ .../output/transforms/richTransform.ts | 41 ++++++- .../browser/renderers/cellViewModel.ts | 85 +++++++++++++-- .../notebook/browser/renderers/codeCell.ts | 5 +- .../contrib/notebook/common/notebookCommon.ts | 2 + 14 files changed, 382 insertions(+), 60 deletions(-) create mode 100644 src/vs/workbench/contrib/notebook/browser/notebookFindWidget.ts diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index 644a224686f..1f05f099c7e 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -623,6 +623,12 @@ export interface ITextModel { */ equalsTextBuffer(other: ITextBuffer): boolean; + /** + * Get the underling text buffer. + * @internal + */ + getTextBuffer(): ITextBuffer; + /** * Get the text in a certain range. * @param range The range describing what text to get. diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 9c999e23ce5..0e144a9cae2 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -389,6 +389,11 @@ export class TextModel extends Disposable implements model.ITextModel { return this._buffer.equals(other); } + public getTextBuffer(): model.ITextBuffer { + this._assertNotDisposed(); + return this._buffer; + } + private _emitContentChangedEvent(rawChange: ModelRawContentChangedEvent, change: IModelContentChangedEvent): void { if (this._isDisposing) { // Do not confuse listeners by emitting any event after disposing diff --git a/src/vs/editor/contrib/find/findController.ts b/src/vs/editor/contrib/find/findController.ts index fee09b8429d..ee4e252ff2b 100644 --- a/src/vs/editor/contrib/find/findController.ts +++ b/src/vs/editor/contrib/find/findController.ts @@ -66,6 +66,7 @@ export interface IFindStartOptions { shouldFocus: FindStartFocusAction; shouldAnimate: boolean; updateSearchScope: boolean; + shouldReveal?: boolean; } export class CommonFindController extends Disposable implements IEditorContribution { @@ -264,6 +265,10 @@ export class CommonFindController extends Disposable implements IEditorContribut isRevealed: true }; + if (opts.shouldReveal !== undefined) { + stateChanges.isRevealed = opts.shouldReveal; + } + if (opts.seedSearchStringFromSelection) { let selectionSearchString = getSelectionSearchString(this._editor); if (selectionSearchString) { diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index ea6205a7970..5b88cfc49e4 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -11,6 +11,7 @@ import { INotebookService, IMainNotebookController } from 'vs/workbench/contrib/ import { Emitter, Event } from 'vs/base/common/event'; import { ICell, IOutput, INotebook, INotebookMimeTypeSelector, NOTEBOOK_DISPLAY_ORDER, NotebookCellsSplice, NotebookCellOutputsSplice, generateCellPath } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { PieceTreeTextBufferFactory, PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder'; export class MainThreadCell implements ICell { private _onDidChangeOutputs = new Emitter(); @@ -38,6 +39,8 @@ export class MainThreadCell implements ICell { readonly uri: URI; + private _buffer: PieceTreeTextBufferFactory | null = null; + constructor( parent: MainThreadNotebookDocument, public handle: number, @@ -66,6 +69,17 @@ export class MainThreadCell implements ICell { save() { this._isDirty = false; } + + resolveTextBufferFactory(): PieceTreeTextBufferFactory { + if (this._buffer) { + return this._buffer; + } + + let builder = new PieceTreeTextBufferBuilder(); + builder.acceptChunk(this.source.join('\n')); + this._buffer = builder.finish(true); + return this._buffer; + } } export class MainThreadNotebookDocument extends Disposable implements INotebook { diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 64c47d7153f..c8d96c3ce37 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -161,8 +161,9 @@ class CellContentProvider implements ITextModelContentProvider { } for (let cell of notebook.cells) { if (cell.uri.toString() === resource.toString()) { + let bufferFactory = cell.resolveTextBufferFactory(); return this._modelService.createModel( - cell.source.join('\n'), + bufferFactory, cell.language ? this._modeService.create(cell.language) : this._modeService.createByFilepathOrFirstLine(resource, cell.source[0]), resource ); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookActions.ts b/src/vs/workbench/contrib/notebook/browser/notebookActions.ts index 058bf9b0e0b..5f813ecac74 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookActions.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { KeyCode } from 'vs/base/common/keyCodes'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { MenuRegistry, MenuId, Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { InputFocusedContext, InputFocusedContextKey } from 'vs/platform/contextkey/common/contextkeys'; @@ -12,6 +12,7 @@ import { INotebookService } from 'vs/workbench/contrib/notebook/browser/notebook import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { NOTEBOOK_EDITOR_FOCUSED, NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEditor'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; registerAction2(class extends Action2 { constructor() { @@ -82,25 +83,15 @@ registerAction2(class extends Action2 { async run(accessor: ServicesAccessor): Promise { let editorService = accessor.get(IEditorService); let notebookService = accessor.get(INotebookService); + let editor = getActiveNotebookEditor(editorService, notebookService); - let resource = editorService.activeEditor?.resource; - let editorControl = editorService.activeControl; - let notebookProviders = notebookService.getContributedNotebookProviders(resource!); - - if (!resource || !editorControl || notebookProviders.length === 0) { + if (!editor) { return; } - let editorViewType = (editorControl! as NotebookEditor).viewType; - let viewType = notebookProviders[0].id; - - if (viewType !== editorViewType) { - return; - } - - let activeCell = (editorControl! as NotebookEditor).getActiveCell(); + let activeCell = editor.getActiveCell(); if (activeCell) { - (editorControl! as NotebookEditor).focusNotebookCell(activeCell, false); + editor.focusNotebookCell(activeCell, false); } } }); @@ -125,29 +116,62 @@ registerAction2(class extends Action2 { async run(accessor: ServicesAccessor): Promise { let editorService = accessor.get(IEditorService); let notebookService = accessor.get(INotebookService); + let editor = getActiveNotebookEditor(editorService, notebookService); - let resource = editorService.activeEditor?.resource; - let editorControl = editorService.activeControl; - let notebookProviders = notebookService.getContributedNotebookProviders(resource!); - - if (!resource || !editorControl || notebookProviders.length === 0) { + if (!editor) { return; } - let editorViewType = (editorControl! as NotebookEditor).viewType; - let viewType = notebookProviders[0].id; - - if (viewType !== editorViewType) { - return; - } - - let activeCell = (editorControl! as NotebookEditor).getActiveCell(); + let activeCell = editor.getActiveCell(); if (activeCell) { - (editorControl! as NotebookEditor).editNotebookCell(undefined, activeCell); + editor.editNotebookCell(undefined, activeCell); } } }); +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.notebook.hideFind', + title: 'Hide Find in Notebook', + keybinding: { + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED), + primary: KeyCode.Escape, + weight: KeybindingWeight.WorkbenchContrib + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + let editorService = accessor.get(IEditorService); + let notebookService = accessor.get(INotebookService); + let editor = getActiveNotebookEditor(editorService, notebookService); + + editor?.hideFind(); + } +}); +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.notebook.find', + title: 'Find in Notebook', + keybinding: { + when: NOTEBOOK_EDITOR_FOCUSED, + primary: KeyCode.KEY_F | KeyMod.CtrlCmd, + weight: KeybindingWeight.WorkbenchContrib + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + let editorService = accessor.get(IEditorService); + let notebookService = accessor.get(INotebookService); + let editor = getActiveNotebookEditor(editorService, notebookService); + + editor?.showFind(); + } +}); + MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: 'workbench.action.executeNotebook', @@ -170,3 +194,23 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { group: 'navigation', when: NOTEBOOK_EDITOR_FOCUSED }); + + +function getActiveNotebookEditor(editorService: IEditorService, notebookService: INotebookService): NotebookEditor | undefined { + let resource = editorService.activeEditor?.resource; + let editorControl = editorService.activeControl; + let notebookProviders = notebookService.getContributedNotebookProviders(resource!); + + if (!resource || !editorControl || notebookProviders.length === 0) { + return; + } + + let editorViewType = (editorControl! as NotebookEditor).viewType; + let viewType = notebookProviders[0].id; + + if (viewType !== editorViewType) { + return; + } + + return editorControl! as NotebookEditor; +} diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 7095abd02f7..ed80b8a57bf 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -10,6 +10,9 @@ import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/renderers/cellViewModel'; import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/output/outputRenderer'; import { IOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; + +export const KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED = new RawContextKey('notebookFindWidgetFocused', false); export interface INotebookEditor { viewType: string | undefined; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index f14dbc13ac2..48abcfe10ab 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -28,7 +28,7 @@ import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/output/out import { BackLayerWebView } from 'vs/workbench/contrib/notebook/browser/renderers/backLayerWebView'; import { CodeCellRenderer, MarkdownCellRenderer, NotebookCellListDelegate } from 'vs/workbench/contrib/notebook/browser/renderers/cellRenderer'; import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/renderers/cellViewModel'; -import { CELL_MARGIN, INotebook, NotebookCellsSplice, IOutput, parseCellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CELL_MARGIN, NotebookCellsSplice, IOutput, parseCellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IWebviewService } from 'vs/workbench/contrib/webview/browser/webview'; import { getExtraColor } from 'vs/workbench/contrib/welcome/walkThrough/common/walkThroughUtils'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -37,6 +37,8 @@ import { IEditor } from 'vs/editor/common/editorCommon'; import { IResourceInput } from 'vs/platform/editor/common/editor'; import { Emitter, Event } from 'vs/base/common/event'; import { NotebookCellList } from 'vs/workbench/contrib/notebook/browser/notebookCellList'; +import { NotebookFindWidget, NotebookFindDelegate, CellFindMatch } from 'vs/workbench/contrib/notebook/browser/notebookFindWidget'; +import { FindMatch } from 'vs/editor/common/model'; const $ = DOM.$; const NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'NotebookEditorViewState'; @@ -107,7 +109,7 @@ class NotebookCodeEditors implements ICompositeCodeEditor { } } -export class NotebookEditor extends BaseEditor implements INotebookEditor { +export class NotebookEditor extends BaseEditor implements INotebookEditor, NotebookFindDelegate { static readonly ID: string = 'workbench.editor.notebook'; private rootElement!: HTMLElement; private body!: HTMLElement; @@ -118,7 +120,6 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { private control: ICompositeCodeEditor | undefined; private renderedEditors: Map = new Map(); private model: NotebookEditorModel | undefined; - private notebook: INotebook | undefined; viewType: string | undefined; private viewCells: CellViewModel[] = []; private localStore: DisposableStore = this._register(new DisposableStore()); @@ -128,6 +129,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { private dimension: DOM.Dimension | null = null; private editorFocus: IContextKey | null = null; private outputRenderer: OutputRenderer; + private findWidget: NotebookFindWidget; constructor( @ITelemetryService telemetryService: ITelemetryService, @@ -145,9 +147,10 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { this.editorMemento = this.getEditorMemento(editorGroupService, NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY); this.outputRenderer = new OutputRenderer(this, this.instantiationService); + this.findWidget = this.instantiationService.createInstance(NotebookFindWidget, this); + this.findWidget.updateTheme(this.themeService.getTheme()); } - get minimumWidth(): number { return 375; } get maximumWidth(): number { return Number.POSITIVE_INFINITY; } @@ -186,6 +189,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { this.contentWidgets = document.createElement('div'); DOM.addClass(this.contentWidgets, 'notebook-content-widgets'); DOM.append(this.body, this.contentWidgets); + DOM.append(this.body, this.findWidget.getDomNode()); } private createCellList(): void { @@ -242,7 +246,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { } onHide() { - + this.editorFocus?.set(false); if (this.webview) { this.localStore.clear(); this.list?.rowsContainer.removeChild(this.webview?.element); @@ -253,9 +257,8 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { this.list?.splice(0, this.list?.length); if (this.model && !this.model.isDirty()) { - this.notebookService.destoryNotebookDocument(this.viewType!, this.notebook!); + this.notebookService.destoryNotebookDocument(this.viewType!, this.model!.notebook!); this.model = undefined; - this.notebook = undefined; this.viewType = undefined; } @@ -273,6 +276,11 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { } } + focus() { + super.focus(); + this.editorFocus?.set(true); + } + setInput(input: NotebookEditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { if (this.input instanceof NotebookEditorInput) { this.saveTextEditorViewState(this.input); @@ -306,12 +314,11 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { })); let viewState = this.loadTextEditorViewState(input); - this.notebook = model.getNotebook(); - this.webview.updateRendererPreloads(this.notebook.renderers); + this.webview.updateRendererPreloads(this.model!.notebook.renderers); this.viewType = input.viewType; - this.viewCells = await Promise.all(this.notebook!.cells.map(async cell => { + this.viewCells = await Promise.all(this.model!.notebook!.cells.map(async cell => { const isEditing = viewState && viewState.editingCells[cell.handle]; - const viewCell = this.instantiationService.createInstance(CellViewModel, input.viewType!, this.notebook!.handle, cell, !!isEditing); + const viewCell = this.instantiationService.createInstance(CellViewModel, input.viewType!, this.model!.notebook!.handle, cell, !!isEditing); this.localStore.add(viewCell); return viewCell; })); @@ -395,6 +402,37 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { //#endregion + //#region Find Delegate + startFind(value: string): CellFindMatch[] { + let matches: CellFindMatch[] = []; + this.viewCells.forEach(cell => { + let cellMatches = cell.startFind(value); + matches.push(...cellMatches); + }); + + return matches; + } + + stopFind(keepSelection?: boolean | undefined): void { + } + + focusNext(match: CellFindMatch) { + let cell = match.cell; + let index = this.viewCells.indexOf(cell); + + this.list?.reveal(index); + } + + public showFind() { + this.findWidget.reveal(); + } + + public hideFind() { + this.findWidget.hide(); + } + + //#endregion + //#region Cell operations layoutNotebookCell(cell: CellViewModel, height: number) { let relayout = (cell: CellViewModel, height: number) => { @@ -413,7 +451,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { updateViewCells(splices: NotebookCellsSplice[]) { let update = () => splices.reverse().forEach((diff) => { this.list?.splice(diff[0], diff[1], diff[2].map(cell => { - return this.instantiationService.createInstance(CellViewModel, this.viewType!, this.notebook!.handle, cell, false); + return this.instantiationService.createInstance(CellViewModel, this.viewType!, this.model!.notebook!.handle, cell, false); })); }); @@ -433,7 +471,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { } async insertEmptyNotebookCell(listIndex: number | undefined, cell: CellViewModel, type: 'code' | 'markdown', direction: 'above' | 'below'): Promise { - let newLanguages = this.notebook!.languages; + let newLanguages = this.model!.notebook!.languages; let language = 'markdown'; if (newLanguages && newLanguages.length) { language = newLanguages[0]; @@ -442,8 +480,8 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { let index = listIndex ? listIndex : this.model!.getNotebook().cells.indexOf(cell.cell); const insertIndex = direction === 'above' ? index : index + 1; - let newModeCell = await this.notebookService.createNotebookCell(this.viewType!, this.notebook!.uri, insertIndex, language, type); - let newCell = this.instantiationService.createInstance(CellViewModel, this.viewType!, this.notebook!.handle, newModeCell!, false); + let newModeCell = await this.notebookService.createNotebookCell(this.viewType!, this.model!.notebook!.uri, insertIndex, language, type); + let newCell = this.instantiationService.createInstance(CellViewModel, this.viewType!, this.model!.notebook!.handle, newModeCell!, false); this.viewCells!.splice(insertIndex, 0, newCell); this.model!.insertCell(newCell.cell, insertIndex); @@ -498,8 +536,8 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { async deleteNotebookCell(listIndex: number | undefined, cell: CellViewModel): Promise { let index = this.model!.getNotebook().cells.indexOf(cell.cell); - // await this.notebookService.createNotebookCell(this.viewType!, this.notebook!.uri, insertIndex, language, type); - await this.notebookService.deleteNotebookCell(this.viewType!, this.notebook!.uri, index); + // await this.notebookService.createNotebookCell(this.viewType!, this.model!.notebook!.uri, insertIndex, language, type); + await this.notebookService.deleteNotebookCell(this.viewType!, this.model!.notebook!.uri, index); this.viewCells!.splice(index, 1); this.model!.deleteCell(cell.cell); this.list?.splice(index, 1); @@ -526,7 +564,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { return; } - let preloads = this.notebook!.renderers; + let preloads = this.model!.notebook!.renderers; if (!this.webview!.insetMapping.has(output)) { let index = this.model!.getNotebook().cells.indexOf(cell.cell); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts index d0ce1ae8b12..cb0bdf5f189 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts @@ -18,6 +18,11 @@ export class NotebookEditorModel extends EditorModel { private readonly _onDidChangeCells = new Emitter(); get onDidChangeCells(): Event { return this._onDidChangeCells.event; } + + get notebook() { + return this._notebook; + } + constructor( private _notebook: INotebook ) { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookFindWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookFindWidget.ts new file mode 100644 index 00000000000..f65d4ca9986 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/notebookFindWidget.ts @@ -0,0 +1,92 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { SimpleFindWidget } from 'vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { Event } from 'vs/base/common/event'; +import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/renderers/cellViewModel'; +import { FindMatch } from 'vs/editor/common/model'; + +export interface CellFindMatch { + cell: CellViewModel, + match: FindMatch +} + +export interface NotebookFindDelegate { + startFind(value: string): CellFindMatch[]; + stopFind(keepSelection?: boolean): void; + focus(): void; + focusNext(nextMatch: CellFindMatch): void; +} + + +export class NotebookFindWidget extends SimpleFindWidget { + protected _findWidgetFocused: IContextKey; + private _findMatches: CellFindMatch[] = []; + private _currentMatch: CellFindMatch | null = null; + + constructor( + private readonly _delegate: NotebookFindDelegate, + @IContextViewService contextViewService: IContextViewService, + @IContextKeyService contextKeyService: IContextKeyService + ) { + super(contextViewService, contextKeyService); + this._findWidgetFocused = KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED.bindTo(contextKeyService); + } + + protected onInputChanged(): boolean { + const val = this.inputValue; + if (val) { + this._findMatches = this._delegate.startFind(val); + if (this._findMatches.length) { + this._currentMatch = this._findMatches[0]; + return true; + } else { + this._currentMatch = null; + return false; + } + } else { + this._delegate.stopFind(false); + } + return false; + } + + protected find(previous: boolean): void { + if (this._currentMatch && this._findMatches.length) { + let len = this._findMatches.length; + let index = this._findMatches.indexOf(this._currentMatch); + + let nextIndex = previous ? (index - 1 + len) % len : index + 1 % len; + let nextMatch = this._findMatches[nextIndex]; + + this._delegate.focusNext(nextMatch); + this._currentMatch = nextMatch; + } + + return; + } + + public hide() { + super.hide(); + this._delegate.stopFind(true); + this._delegate.focus(); + } + + protected findFirst(): void { } + + protected onFocusTrackerFocus() { + this._findWidgetFocused.set(true); + } + + protected onFocusTrackerBlur() { + this._findWidgetFocused.reset(); + } + + protected onFindInputFocusTrackerFocus(): void { } + protected onFindInputFocusTrackerBlur(): void { } + +} diff --git a/src/vs/workbench/contrib/notebook/browser/output/transforms/richTransform.ts b/src/vs/workbench/contrib/notebook/browser/output/transforms/richTransform.ts index b41350c5f63..01ddb43f8d8 100644 --- a/src/vs/workbench/contrib/notebook/browser/output/transforms/richTransform.ts +++ b/src/vs/workbench/contrib/notebook/browser/output/transforms/richTransform.ts @@ -34,6 +34,7 @@ class RichRenderer implements IOutputTransformContribution { this._richMimeTypeRenderers.set('image/png', this.renderPNG.bind(this)); this._richMimeTypeRenderers.set('image/jpeg', this.renderJavaScript.bind(this)); this._richMimeTypeRenderers.set('text/plain', this.renderPlainText.bind(this)); + this._richMimeTypeRenderers.set('text/x-javascript', this.renderCode.bind(this)); } render(output: any, container: HTMLElement, preferredMimeType: string | undefined): IRenderOutput { @@ -73,7 +74,7 @@ class RichRenderer implements IOutputTransformContribution { let str = JSON.stringify(data, null, '\t'); const editor = this.instantiationService.createInstance(CodeEditorWidget, container, { - ...getJSONSimpleEditorOptions(), + ...getOutputSimpleEditorOptions(), dimension: { width: 0, height: 0 @@ -103,6 +104,41 @@ class RichRenderer implements IOutputTransformContribution { }; } + renderCode(output: any, container: HTMLElement) { + let data = output.data['text/x-javascript']; + let str = isArray(data) ? data.join('') : data; + + const editor = this.instantiationService.createInstance(CodeEditorWidget, container, { + ...getOutputSimpleEditorOptions(), + dimension: { + width: 0, + height: 0 + } + }, { + isSimpleWidget: true + }); + + let mode = this.modeService.create('javascript'); + let resource = URI.parse(`notebook-output-${Date.now()}.js`); + const textModel = this.modelService.createModel(str, mode, resource, false); + editor.setModel(textModel); + + let width = this.notebookEditor.getListDimension()!.width; + let fontInfo = this.notebookEditor.getFontInfo(); + let height = Math.min(textModel.getLineCount(), 16) * (fontInfo?.lineHeight || 18); + + editor.layout({ + height, + width + }); + + container.style.height = `${height + 16}px`; + + return { + hasDynamicHeight: true + }; + } + renderJavaScript(output: any, container: HTMLElement) { let data = output.data['application/javascript']; let str = isArray(data) ? data.join('') : data; @@ -188,8 +224,9 @@ class RichRenderer implements IOutputTransformContribution { registerOutputTransform('notebook.output.rich', ['display_data', 'execute_result'], RichRenderer); -export function getJSONSimpleEditorOptions(): IEditorOptions { +export function getOutputSimpleEditorOptions(): IEditorOptions { return { + readOnly: true, wordWrap: 'on', overviewRulerLanes: 0, glyphMargin: false, diff --git a/src/vs/workbench/contrib/notebook/browser/renderers/cellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/renderers/cellViewModel.ts index 2e56f77d475..6260430995d 100644 --- a/src/vs/workbench/contrib/notebook/browser/renderers/cellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/renderers/cellViewModel.ts @@ -3,15 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; -import { ITextModel } from 'vs/editor/common/model'; -import { Emitter } from 'vs/base/common/event'; import * as UUID from 'vs/base/common/uuid'; -import { MarkdownRenderer } from 'vs/workbench/contrib/notebook/browser/renderers/mdRenderer'; -import { ICell, NotebookCellOutputsSplice, IOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { Range } from 'vs/editor/common/core/range'; +import * as model from 'vs/editor/common/model'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { PrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { CellFindMatch } from 'vs/workbench/contrib/notebook/browser/notebookFindWidget'; +import { MarkdownRenderer } from 'vs/workbench/contrib/notebook/browser/renderers/mdRenderer'; +import { EDITOR_BOTTOM_PADDING, EDITOR_TOP_PADDING, ICell, IOutput, NotebookCellOutputsSplice } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { SearchParams } from 'vs/editor/common/model/textModelSearch'; export class CellViewModel extends Disposable { @@ -63,7 +67,10 @@ export class CellViewModel extends Disposable { return this._editorHeight; } - private _textModel?: ITextModel; + private _textModel?: model.ITextModel; + private _textEditor?: ICodeEditor; + private _buffer: model.ITextBuffer | null; + readonly id: string = UUID.generateUuid(); constructor( @@ -84,7 +91,60 @@ export class CellViewModel extends Disposable { } this._outputCollection = new Array(this.cell.outputs.length); + this._buffer = null; } + + + //#region Search + private readonly _hasFindResult = this._register(new Emitter()); + public readonly hasFindResult: Event = this._hasFindResult.event; + + startFind(value: string): CellFindMatch[] { + let cellMatches: model.FindMatch[] = []; + if (this.assertTextModelAttached()) { + cellMatches = this._textModel!.findMatches(value, false, false, false, null, false); + } else { + if (!this._buffer) { + this._buffer = this.cell.resolveTextBufferFactory().create(model.DefaultEndOfLine.LF); + } + + const lineCount = this._buffer.getLineCount(); + const fullRange = new Range(1, 1, lineCount, this._buffer.getLineLength(lineCount) + 1); + const searchParams = new SearchParams(value, false, false, null); + const searchData = searchParams.parseSearchRequest(); + + if (!searchData) { + return []; + } + + cellMatches = this._buffer.findMatchesLineByLine(fullRange, searchData, false, 1000); + } + + return cellMatches.map(match => ({ + cell: this, + match: match + })); + } + + stopFind(keepSelection?: boolean | undefined): void { + if (!this.assertTextModelAttached()) { + return; + } + } + + focus(): void { + } + + assertTextModelAttached(): boolean { + if (this._textModel && this._textEditor && this._textEditor.getModel() === this._textModel) { + return true; + } + + return false; + } + + //#endregion + hasDynamicHeight() { if (this._dynamicHeight !== null) { return false; @@ -118,7 +178,7 @@ export class CellViewModel extends Disposable { return 100; } else { - return this.lineCount * lineHeight + 16; + return this.lineCount * lineHeight + 16 + EDITOR_TOP_PADDING + EDITOR_BOTTOM_PADDING; } } setText(strs: string[]) { @@ -132,6 +192,10 @@ export class CellViewModel extends Disposable { } } getText(): string { + if (this._textModel) { + return this._textModel.getValue(); + } + return this.cell.source.join('\n'); } @@ -147,10 +211,11 @@ export class CellViewModel extends Disposable { return null; } - async resolveTextModel(): Promise { + async resolveTextModel(): Promise { if (!this._textModel) { const ref = await this._modelService.createModelReference(this.cell.uri); this._textModel = ref.object.textEditorModel; + this._buffer = this._textModel.getTextBuffer(); this._register(ref); this._register(this._textModel.onDidChangeContent(() => { this.cell.isDirty = true; @@ -159,6 +224,10 @@ export class CellViewModel extends Disposable { return this._textModel; } + attachTextEditor(editor: ICodeEditor) { + this._textEditor = editor; + } + getMarkdownRenderer() { if (!this._mdRenderer) { this._mdRenderer = this._instaService.createInstance(MarkdownRenderer); diff --git a/src/vs/workbench/contrib/notebook/browser/renderers/codeCell.ts b/src/vs/workbench/contrib/notebook/browser/renderers/codeCell.ts index a3947c5652e..61aaa3f03ba 100644 --- a/src/vs/workbench/contrib/notebook/browser/renderers/codeCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/renderers/codeCell.ts @@ -47,8 +47,9 @@ export class CodeCell extends Disposable { const cts = new CancellationTokenSource(); this._register({ dispose() { cts.dispose(true); } }); raceCancellation(viewCell.resolveTextModel(), cts.token).then(model => { - if (model) { - templateData.editor?.setModel(model); + if (model && templateData.editor) { + templateData.editor.setModel(model); + viewCell.attachTextEditor(templateData.editor); let realContentHeight = templateData.editor?.getContentHeight(); let width: number; diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 9605f5d354d..1a12bad7a01 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -7,6 +7,7 @@ import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IRelativePattern } from 'vs/base/common/glob'; +import { PieceTreeTextBufferFactory } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder'; export const NOTEBOOK_DISPLAY_ORDER = [ 'application/json', @@ -103,6 +104,7 @@ export interface ICell { outputs: IOutput[]; onDidChangeOutputs?: Event; isDirty: boolean; + resolveTextBufferFactory(): PieceTreeTextBufferFactory; } /**