Path: blob/main/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Emitter, Event } from '../../../../../base/common/event.js';6import { Disposable, IDisposable, IReference, MutableDisposable, dispose } from '../../../../../base/common/lifecycle.js';7import { Mimes } from '../../../../../base/common/mime.js';8import { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js';9import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js';10import { IEditorCommentsOptions } from '../../../../../editor/common/config/editorOptions.js';11import { IPosition } from '../../../../../editor/common/core/position.js';12import { IRange, Range } from '../../../../../editor/common/core/range.js';13import { Selection } from '../../../../../editor/common/core/selection.js';14import * as editorCommon from '../../../../../editor/common/editorCommon.js';15import * as model from '../../../../../editor/common/model.js';16import { SearchParams } from '../../../../../editor/common/model/textModelSearch.js';17import { IResolvedTextEditorModel, ITextModelService } from '../../../../../editor/common/services/resolverService.js';18import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';19import { IUndoRedoService } from '../../../../../platform/undoRedo/common/undoRedo.js';20import { IWordWrapTransientState, readTransientState, writeTransientState } from '../../../codeEditor/browser/toggleWordWrap.js';21import { CellEditState, CellFocusMode, CellLayoutChangeEvent, CursorAtBoundary, CursorAtLineBoundary, IEditableCellViewModel, INotebookCellDecorationOptions } from '../notebookBrowser.js';22import { NotebookOptionsChangeEvent } from '../notebookOptions.js';23import { CellViewModelStateChangeEvent } from '../notebookViewEvents.js';24import { ViewContext } from './viewContext.js';25import { NotebookCellTextModel } from '../../common/model/notebookCellTextModel.js';26import { CellKind, INotebookCellStatusBarItem, INotebookFindOptions } from '../../common/notebookCommon.js';27import { IInlineChatSessionService } from '../../../inlineChat/browser/inlineChatSessionService.js';2829export abstract class BaseCellViewModel extends Disposable {3031protected readonly _onDidChangeEditorAttachState = this._register(new Emitter<void>());32// Do not merge this event with `onDidChangeState` as we are using `Event.once(onDidChangeEditorAttachState)` elsewhere.33readonly onDidChangeEditorAttachState = this._onDidChangeEditorAttachState.event;34protected readonly _onDidChangeState = this._register(new Emitter<CellViewModelStateChangeEvent>());35public readonly onDidChangeState: Event<CellViewModelStateChangeEvent> = this._onDidChangeState.event;3637get handle() {38return this.model.handle;39}40get uri() {41return this.model.uri;42}43get lineCount() {44return this.model.textBuffer.getLineCount();45}46get metadata() {47return this.model.metadata;48}49get internalMetadata() {50return this.model.internalMetadata;51}52get language() {53return this.model.language;54}5556get mime(): string {57if (typeof this.model.mime === 'string') {58return this.model.mime;59}6061switch (this.language) {62case 'markdown':63return Mimes.markdown;6465default:66return Mimes.text;67}68}6970abstract cellKind: CellKind;7172private _editState: CellEditState = CellEditState.Preview;7374private _lineNumbers: 'on' | 'off' | 'inherit' = 'inherit';75get lineNumbers(): 'on' | 'off' | 'inherit' {76return this._lineNumbers;77}7879set lineNumbers(lineNumbers: 'on' | 'off' | 'inherit') {80if (lineNumbers === this._lineNumbers) {81return;82}8384this._lineNumbers = lineNumbers;85this._onDidChangeState.fire({ cellLineNumberChanged: true });86}8788private _commentOptions: IEditorCommentsOptions;89public get commentOptions(): IEditorCommentsOptions {90return this._commentOptions;91}9293public set commentOptions(newOptions: IEditorCommentsOptions) {94this._commentOptions = newOptions;95}9697private _focusMode: CellFocusMode = CellFocusMode.Container;98get focusMode() {99return this._focusMode;100}101set focusMode(newMode: CellFocusMode) {102if (this._focusMode !== newMode) {103this._focusMode = newMode;104this._onDidChangeState.fire({ focusModeChanged: true });105}106}107108protected _textEditor?: ICodeEditor;109get editorAttached(): boolean {110return !!this._textEditor;111}112private _editorListeners: IDisposable[] = [];113private _editorViewStates: editorCommon.ICodeEditorViewState | null = null;114private _editorTransientState: IWordWrapTransientState | null = null;115private _resolvedCellDecorations = new Map<string, INotebookCellDecorationOptions>();116private readonly _textModelRefChangeDisposable = this._register(new MutableDisposable());117118private readonly _cellDecorationsChanged = this._register(new Emitter<{ added: INotebookCellDecorationOptions[]; removed: INotebookCellDecorationOptions[] }>());119onCellDecorationsChanged: Event<{ added: INotebookCellDecorationOptions[]; removed: INotebookCellDecorationOptions[] }> = this._cellDecorationsChanged.event;120121private _resolvedDecorations = new Map<string, {122id?: string;123options: model.IModelDeltaDecoration;124}>();125private _lastDecorationId: number = 0;126127private _cellStatusBarItems = new Map<string, INotebookCellStatusBarItem>();128private readonly _onDidChangeCellStatusBarItems = this._register(new Emitter<void>());129readonly onDidChangeCellStatusBarItems: Event<void> = this._onDidChangeCellStatusBarItems.event;130private _lastStatusBarId: number = 0;131132get textModel(): model.ITextModel | undefined {133return this.model.textModel;134}135136hasModel(): this is IEditableCellViewModel {137return !!this.textModel;138}139140private _dragging: boolean = false;141get dragging(): boolean {142return this._dragging;143}144145set dragging(v: boolean) {146this._dragging = v;147this._onDidChangeState.fire({ dragStateChanged: true });148}149150protected _textModelRef: IReference<IResolvedTextEditorModel> | undefined;151152private _inputCollapsed: boolean = false;153get isInputCollapsed(): boolean {154return this._inputCollapsed;155}156set isInputCollapsed(v: boolean) {157this._inputCollapsed = v;158this._onDidChangeState.fire({ inputCollapsedChanged: true });159}160161private _outputCollapsed: boolean = false;162get isOutputCollapsed(): boolean {163return this._outputCollapsed;164}165set isOutputCollapsed(v: boolean) {166this._outputCollapsed = v;167this._onDidChangeState.fire({ outputCollapsedChanged: true });168}169170protected _commentHeight = 0;171172set commentHeight(height: number) {173if (this._commentHeight === height) {174return;175}176this._commentHeight = height;177this.layoutChange({ commentHeight: true }, 'BaseCellViewModel#commentHeight');178}179180private _isDisposed = false;181private _isReadonly = false;182183constructor(184readonly viewType: string,185readonly model: NotebookCellTextModel,186public id: string,187private readonly _viewContext: ViewContext,188private readonly _configurationService: IConfigurationService,189private readonly _modelService: ITextModelService,190private readonly _undoRedoService: IUndoRedoService,191private readonly _codeEditorService: ICodeEditorService,192private readonly _inlineChatSessionService: IInlineChatSessionService193// private readonly _keymapService: INotebookKeymapService194) {195super();196197this._register(model.onDidChangeMetadata(() => {198this._onDidChangeState.fire({ metadataChanged: true });199}));200201this._register(model.onDidChangeInternalMetadata(e => {202this._onDidChangeState.fire({ internalMetadataChanged: true });203if (e.lastRunSuccessChanged) {204// Statusbar visibility may change205this.layoutChange({});206}207}));208209this._register(this._configurationService.onDidChangeConfiguration(e => {210if (e.affectsConfiguration('notebook.lineNumbers')) {211this.lineNumbers = 'inherit';212}213}));214215if (this.model.collapseState?.inputCollapsed) {216this._inputCollapsed = true;217}218219if (this.model.collapseState?.outputCollapsed) {220this._outputCollapsed = true;221}222223this._commentOptions = this._configurationService.getValue<IEditorCommentsOptions>('editor.comments', { overrideIdentifier: this.language });224this._register(this._configurationService.onDidChangeConfiguration(e => {225if (e.affectsConfiguration('editor.comments')) {226this._commentOptions = this._configurationService.getValue<IEditorCommentsOptions>('editor.comments', { overrideIdentifier: this.language });227}228}));229}230231232updateOptions(e: NotebookOptionsChangeEvent): void {233if (this._textEditor && typeof e.readonly === 'boolean') {234this._textEditor.updateOptions({ readOnly: e.readonly });235}236if (typeof e.readonly === 'boolean') {237this._isReadonly = e.readonly;238}239}240abstract getHeight(lineHeight: number): number;241abstract onDeselect(): void;242abstract layoutChange(change: CellLayoutChangeEvent, source?: string): void;243244assertTextModelAttached(): boolean {245if (this.textModel && this._textEditor && this._textEditor.getModel() === this.textModel) {246return true;247}248249return false;250}251252// private handleKeyDown(e: IKeyboardEvent) {253// if (this.viewType === IPYNB_VIEW_TYPE && isWindows && e.ctrlKey && e.keyCode === KeyCode.Enter) {254// this._keymapService.promptKeymapRecommendation();255// }256// }257258attachTextEditor(editor: ICodeEditor, estimatedHasHorizontalScrolling?: boolean) {259if (!editor.hasModel()) {260throw new Error('Invalid editor: model is missing');261}262263if (this._textEditor === editor) {264if (this._editorListeners.length === 0) {265this._editorListeners.push(this._textEditor.onDidChangeCursorSelection(() => { this._onDidChangeState.fire({ selectionChanged: true }); }));266// this._editorListeners.push(this._textEditor.onKeyDown(e => this.handleKeyDown(e)));267this._onDidChangeState.fire({ selectionChanged: true });268}269return;270}271272this._textEditor = editor;273if (this._isReadonly) {274editor.updateOptions({ readOnly: this._isReadonly });275}276if (this._editorViewStates) {277this._restoreViewState(this._editorViewStates);278} else {279// If no real editor view state was persisted, restore a default state.280// This forces the editor to measure its content width immediately.281if (estimatedHasHorizontalScrolling) {282this._restoreViewState({283contributionsState: {},284cursorState: [],285viewState: {286scrollLeft: 0,287firstPosition: { lineNumber: 1, column: 1 },288firstPositionDeltaTop: this._viewContext.notebookOptions.getLayoutConfiguration().editorTopPadding289}290});291}292}293294if (this._editorTransientState) {295writeTransientState(editor.getModel(), this._editorTransientState, this._codeEditorService);296}297298if (this._isDisposed) {299// Restore View State could adjust the editor layout and trigger a list view update. The list view update might then dispose this view model.300return;301}302303editor.changeDecorations((accessor) => {304this._resolvedDecorations.forEach((value, key) => {305if (key.startsWith('_lazy_')) {306// lazy ones307const ret = accessor.addDecoration(value.options.range, value.options.options);308this._resolvedDecorations.get(key)!.id = ret;309}310else {311const ret = accessor.addDecoration(value.options.range, value.options.options);312this._resolvedDecorations.get(key)!.id = ret;313}314});315});316317this._editorListeners.push(editor.onDidChangeCursorSelection(() => { this._onDidChangeState.fire({ selectionChanged: true }); }));318this._editorListeners.push(this._inlineChatSessionService.onWillStartSession((e) => {319if (e === this._textEditor && this.textBuffer.getLength() === 0) {320this.enableAutoLanguageDetection();321}322}));323324this._onDidChangeState.fire({ selectionChanged: true });325this._onDidChangeEditorAttachState.fire();326}327328detachTextEditor() {329this.saveViewState();330this.saveTransientState();331// decorations need to be cleared first as editors can be resued.332this._textEditor?.changeDecorations((accessor) => {333this._resolvedDecorations.forEach(value => {334const resolvedid = value.id;335336if (resolvedid) {337accessor.removeDecoration(resolvedid);338}339});340});341342this._textEditor = undefined;343dispose(this._editorListeners);344this._editorListeners = [];345this._onDidChangeEditorAttachState.fire();346347if (this._textModelRef) {348this._textModelRef.dispose();349this._textModelRef = undefined;350}351this._textModelRefChangeDisposable.clear();352}353354getText(): string {355return this.model.getValue();356}357358getAlternativeId(): number {359return this.model.alternativeId;360}361362getTextLength(): number {363return this.model.getTextLength();364}365366enableAutoLanguageDetection() {367this.model.enableAutoLanguageDetection();368}369370private saveViewState(): void {371if (!this._textEditor) {372return;373}374375this._editorViewStates = this._textEditor.saveViewState();376}377378private saveTransientState() {379if (!this._textEditor || !this._textEditor.hasModel()) {380return;381}382383this._editorTransientState = readTransientState(this._textEditor.getModel(), this._codeEditorService);384}385386saveEditorViewState() {387if (this._textEditor) {388this._editorViewStates = this._textEditor.saveViewState();389}390391return this._editorViewStates;392}393394restoreEditorViewState(editorViewStates: editorCommon.ICodeEditorViewState | null, totalHeight?: number) {395this._editorViewStates = editorViewStates;396}397398private _restoreViewState(state: editorCommon.ICodeEditorViewState | null): void {399if (state) {400this._textEditor?.restoreViewState(state);401}402}403404addModelDecoration(decoration: model.IModelDeltaDecoration): string {405if (!this._textEditor) {406const id = ++this._lastDecorationId;407const decorationId = `_lazy_${this.id};${id}`;408this._resolvedDecorations.set(decorationId, { options: decoration });409return decorationId;410}411412let id: string;413this._textEditor.changeDecorations((accessor) => {414id = accessor.addDecoration(decoration.range, decoration.options);415this._resolvedDecorations.set(id, { id, options: decoration });416});417return id!;418}419420removeModelDecoration(decorationId: string) {421const realDecorationId = this._resolvedDecorations.get(decorationId);422423if (this._textEditor && realDecorationId && realDecorationId.id !== undefined) {424this._textEditor.changeDecorations((accessor) => {425accessor.removeDecoration(realDecorationId.id!);426});427}428429// lastly, remove all the cache430this._resolvedDecorations.delete(decorationId);431}432433deltaModelDecorations(oldDecorations: readonly string[], newDecorations: readonly model.IModelDeltaDecoration[]): string[] {434oldDecorations.forEach(id => {435this.removeModelDecoration(id);436});437438const ret = newDecorations.map(option => {439return this.addModelDecoration(option);440});441442return ret;443}444445private _removeCellDecoration(decorationId: string) {446const options = this._resolvedCellDecorations.get(decorationId);447this._resolvedCellDecorations.delete(decorationId);448449if (options) {450for (const existingOptions of this._resolvedCellDecorations.values()) {451// don't remove decorations that are applied from other entries452if (options.className === existingOptions.className) {453options.className = undefined;454}455if (options.outputClassName === existingOptions.outputClassName) {456options.outputClassName = undefined;457}458if (options.gutterClassName === existingOptions.gutterClassName) {459options.gutterClassName = undefined;460}461if (options.topClassName === existingOptions.topClassName) {462options.topClassName = undefined;463}464}465466this._cellDecorationsChanged.fire({ added: [], removed: [options] });467}468}469470private _addCellDecoration(options: INotebookCellDecorationOptions): string {471const id = ++this._lastDecorationId;472const decorationId = `_cell_${this.id};${id}`;473this._resolvedCellDecorations.set(decorationId, options);474this._cellDecorationsChanged.fire({ added: [options], removed: [] });475return decorationId;476}477478getCellDecorations() {479return [...this._resolvedCellDecorations.values()];480}481482getCellDecorationRange(decorationId: string): Range | null {483if (this._textEditor) {484// (this._textEditor as CodeEditorWidget).decora485return this._textEditor.getModel()?.getDecorationRange(decorationId) ?? null;486}487488return null;489}490491deltaCellDecorations(oldDecorations: string[], newDecorations: INotebookCellDecorationOptions[]): string[] {492oldDecorations.forEach(id => {493this._removeCellDecoration(id);494});495496const ret = newDecorations.map(option => {497return this._addCellDecoration(option);498});499500return ret;501}502503deltaCellStatusBarItems(oldItems: readonly string[], newItems: readonly INotebookCellStatusBarItem[]): string[] {504oldItems.forEach(id => {505const item = this._cellStatusBarItems.get(id);506if (item) {507this._cellStatusBarItems.delete(id);508}509});510511const newIds = newItems.map(item => {512const id = ++this._lastStatusBarId;513const itemId = `_cell_${this.id};${id}`;514this._cellStatusBarItems.set(itemId, item);515return itemId;516});517518this._onDidChangeCellStatusBarItems.fire();519520return newIds;521}522523getCellStatusBarItems(): INotebookCellStatusBarItem[] {524return Array.from(this._cellStatusBarItems.values());525}526527revealRangeInCenter(range: Range) {528this._textEditor?.revealRangeInCenter(range, editorCommon.ScrollType.Immediate);529}530531setSelection(range: Range) {532this._textEditor?.setSelection(range);533}534535setSelections(selections: Selection[]) {536if (selections.length) {537if (this._textEditor) {538this._textEditor?.setSelections(selections);539} else if (this._editorViewStates) {540this._editorViewStates.cursorState = selections.map(selection => {541return {542inSelectionMode: !selection.isEmpty(),543selectionStart: selection.getStartPosition(),544position: selection.getEndPosition(),545};546});547}548}549}550551getSelections() {552return this._textEditor?.getSelections()553?? this._editorViewStates?.cursorState.map(state => new Selection(state.selectionStart.lineNumber, state.selectionStart.column, state.position.lineNumber, state.position.column))554?? [];555}556557getSelectionsStartPosition(): IPosition[] | undefined {558if (this._textEditor) {559const selections = this._textEditor.getSelections();560return selections?.map(s => s.getStartPosition());561} else {562const selections = this._editorViewStates?.cursorState;563return selections?.map(s => s.selectionStart);564}565}566567getLineScrollTopOffset(line: number): number {568if (!this._textEditor) {569return 0;570}571572const editorPadding = this._viewContext.notebookOptions.computeEditorPadding(this.internalMetadata, this.uri);573return this._textEditor.getTopForLineNumber(line) + editorPadding.top;574}575576getPositionScrollTopOffset(range: Selection | Range): number {577if (!this._textEditor) {578return 0;579}580581582const position = range instanceof Selection ? range.getPosition() : range.getStartPosition();583584const editorPadding = this._viewContext.notebookOptions.computeEditorPadding(this.internalMetadata, this.uri);585return this._textEditor.getTopForPosition(position.lineNumber, position.column) + editorPadding.top;586}587588cursorAtLineBoundary(): CursorAtLineBoundary {589if (!this._textEditor || !this.textModel || !this._textEditor.hasTextFocus()) {590return CursorAtLineBoundary.None;591}592593const selection = this._textEditor.getSelection();594595if (!selection || !selection.isEmpty()) {596return CursorAtLineBoundary.None;597}598599const currentLineLength = this.textModel.getLineLength(selection.startLineNumber);600601if (currentLineLength === 0) {602return CursorAtLineBoundary.Both;603}604605switch (selection.startColumn) {606case 1:607return CursorAtLineBoundary.Start;608case currentLineLength + 1:609return CursorAtLineBoundary.End;610default:611return CursorAtLineBoundary.None;612}613}614615cursorAtBoundary(): CursorAtBoundary {616if (!this._textEditor) {617return CursorAtBoundary.None;618}619620if (!this.textModel) {621return CursorAtBoundary.None;622}623624// only validate primary cursor625const selection = this._textEditor.getSelection();626627// only validate empty cursor628if (!selection || !selection.isEmpty()) {629return CursorAtBoundary.None;630}631632const firstViewLineTop = this._textEditor.getTopForPosition(1, 1);633const lastViewLineTop = this._textEditor.getTopForPosition(this.textModel.getLineCount(), this.textModel.getLineLength(this.textModel.getLineCount()));634const selectionTop = this._textEditor.getTopForPosition(selection.startLineNumber, selection.startColumn);635636if (selectionTop === lastViewLineTop) {637if (selectionTop === firstViewLineTop) {638return CursorAtBoundary.Both;639} else {640return CursorAtBoundary.Bottom;641}642} else {643if (selectionTop === firstViewLineTop) {644return CursorAtBoundary.Top;645} else {646return CursorAtBoundary.None;647}648}649}650651private _editStateSource: string = '';652653get editStateSource(): string {654return this._editStateSource;655}656657updateEditState(newState: CellEditState, source: string) {658if (newState === this._editState) {659return;660}661662this._editStateSource = source;663this._editState = newState;664this._onDidChangeState.fire({ editStateChanged: true });665if (this._editState === CellEditState.Preview) {666this.focusMode = CellFocusMode.Container;667}668}669670getEditState() {671return this._editState;672}673674get textBuffer() {675return this.model.textBuffer;676}677678/**679* Text model is used for editing.680*/681async resolveTextModel(): Promise<model.ITextModel> {682if (!this._textModelRef || !this.textModel) {683this._textModelRef = await this._modelService.createModelReference(this.uri);684if (this._isDisposed) {685return this.textModel!;686}687688if (!this._textModelRef) {689throw new Error(`Cannot resolve text model for ${this.uri}`);690}691this._textModelRefChangeDisposable.value = this.textModel!.onDidChangeContent(() => this.onDidChangeTextModelContent());692}693694return this.textModel!;695}696697protected abstract onDidChangeTextModelContent(): void;698699protected cellStartFind(value: string, options: INotebookFindOptions): model.FindMatch[] | null {700let cellMatches: model.FindMatch[] = [];701702const lineCount = this.textBuffer.getLineCount();703const findRange: IRange[] = options.findScope?.selectedTextRanges ?? [new Range(1, 1, lineCount, this.textBuffer.getLineLength(lineCount) + 1)];704705if (this.assertTextModelAttached()) {706cellMatches = this.textModel!.findMatches(707value,708findRange,709options.regex || false,710options.caseSensitive || false,711options.wholeWord ? options.wordSeparators || null : null,712options.regex || false);713} else {714const searchParams = new SearchParams(value, options.regex || false, options.caseSensitive || false, options.wholeWord ? options.wordSeparators || null : null,);715const searchData = searchParams.parseSearchRequest();716717if (!searchData) {718return null;719}720721findRange.forEach(range => {722cellMatches.push(...this.textBuffer.findMatchesLineByLine(new Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn), searchData, options.regex || false, 1000));723});724}725726return cellMatches;727}728729override dispose() {730this._isDisposed = true;731super.dispose();732733dispose(this._editorListeners);734735// Only remove the undo redo stack if we map this cell uri to itself736// If we are not in perCell mode, it will map to the full NotebookDocument and737// we don't want to remove that entire document undo / redo stack when a cell is deleted738if (this._undoRedoService.getUriComparisonKey(this.uri) === this.uri.toString()) {739this._undoRedoService.removeElements(this.uri);740}741742this._textModelRef?.dispose();743}744745toJSON(): object {746return {747handle: this.handle748};749}750}751752753