From 9e912411843f32b2a549ef6580b07872edd95e8b Mon Sep 17 00:00:00 2001 From: Mark Sujew Date: Tue, 6 Aug 2024 02:05:41 +0200 Subject: [PATCH] Refactor undo-redo action for editors (#13963) --- .../browser/common-frontend-contribution.ts | 14 ++- .../browser/frontend-application-module.ts | 6 ++ packages/core/src/browser/index.ts | 1 + .../core/src/browser/undo-redo-handler.ts | 85 +++++++++++++++++++ packages/monaco/src/browser/monaco-command.ts | 6 +- .../monaco/src/browser/monaco-editor-model.ts | 8 ++ .../src/browser/monaco-frontend-module.ts | 9 +- .../src/browser/monaco-undo-redo-handler.ts | 64 ++++++++++++++ .../notebook-actions-contribution.ts | 18 +--- .../notebook-undo-redo-handler.ts | 41 +++++++++ .../src/browser/notebook-frontend-module.ts | 6 +- packages/notebook/src/browser/style/index.css | 7 +- .../browser/view-model/notebook-cell-model.ts | 18 ++-- .../src/browser/view-model/notebook-model.ts | 55 +++++++----- .../src/browser/view/notebook-cell-editor.tsx | 2 +- .../browser/view/notebook-cell-list-view.tsx | 9 +- .../view/notebook-markdown-cell-view.tsx | 24 +++--- .../custom-editor-contribution.ts | 38 --------- .../custom-editor-undo-redo-handler.ts | 41 +++++++++ .../custom-editors/custom-editor-widget.ts | 8 +- .../custom-editors/custom-editors-main.ts | 15 +++- .../browser/plugin-ext-frontend-module.ts | 10 +-- 22 files changed, 367 insertions(+), 118 deletions(-) create mode 100644 packages/core/src/browser/undo-redo-handler.ts create mode 100644 packages/monaco/src/browser/monaco-undo-redo-handler.ts create mode 100644 packages/notebook/src/browser/contributions/notebook-undo-redo-handler.ts delete mode 100644 packages/plugin-ext/src/main/browser/custom-editors/custom-editor-contribution.ts create mode 100644 packages/plugin-ext/src/main/browser/custom-editors/custom-editor-undo-redo-handler.ts diff --git a/packages/core/src/browser/common-frontend-contribution.ts b/packages/core/src/browser/common-frontend-contribution.ts index 15fe68122380c..64489486e30f0 100644 --- a/packages/core/src/browser/common-frontend-contribution.ts +++ b/packages/core/src/browser/common-frontend-contribution.ts @@ -67,6 +67,7 @@ import { UserWorkingDirectoryProvider } from './user-working-directory-provider' import { UNTITLED_SCHEME, UntitledResourceResolver } from '../common'; import { LanguageQuickPickService } from './i18n/language-quick-pick-service'; import { SidebarMenu } from './shell/sidebar-menu-widget'; +import { UndoRedoHandlerService } from './undo-redo-handler'; export namespace CommonMenus { @@ -443,6 +444,9 @@ export class CommonFrontendContribution implements FrontendApplicationContributi @inject(UntitledResourceResolver) protected readonly untitledResourceResolver: UntitledResourceResolver; + @inject(UndoRedoHandlerService) + protected readonly undoRedoHandlerService: UndoRedoHandlerService; + protected pinnedKey: ContextKey; protected inputFocus: ContextKey; @@ -814,10 +818,14 @@ export class CommonFrontendContribution implements FrontendApplicationContributi })); commandRegistry.registerCommand(CommonCommands.UNDO, { - execute: () => document.execCommand('undo') + execute: () => { + this.undoRedoHandlerService.undo(); + } }); commandRegistry.registerCommand(CommonCommands.REDO, { - execute: () => document.execCommand('redo') + execute: () => { + this.undoRedoHandlerService.redo(); + } }); commandRegistry.registerCommand(CommonCommands.SELECT_ALL, { execute: () => document.execCommand('selectAll') @@ -1080,7 +1088,7 @@ export class CommonFrontendContribution implements FrontendApplicationContributi }, { command: CommonCommands.REDO.id, - keybinding: 'ctrlcmd+shift+z' + keybinding: isOSX ? 'ctrlcmd+shift+z' : 'ctrlcmd+y' }, { command: CommonCommands.SELECT_ALL.id, diff --git a/packages/core/src/browser/frontend-application-module.ts b/packages/core/src/browser/frontend-application-module.ts index 6ab58faec8bf9..3ef33e6fe8238 100644 --- a/packages/core/src/browser/frontend-application-module.ts +++ b/packages/core/src/browser/frontend-application-module.ts @@ -143,6 +143,7 @@ import { LanguageIconLabelProvider } from './language-icon-provider'; import { bindTreePreferences } from './tree'; import { OpenWithService } from './open-with-service'; import { ViewColumnService } from './shell/view-column-service'; +import { DomInputUndoRedoHandler, UndoRedoHandler, UndoRedoHandlerService } from './undo-redo-handler'; export { bindResourceProvider, bindMessageService, bindPreferenceService }; @@ -466,4 +467,9 @@ export const frontendApplicationModule = new ContainerModule((bind, _unbind, _is bind(SecondaryWindowHandler).toSelf().inSingletonScope(); bind(ViewColumnService).toSelf().inSingletonScope(); + + bind(UndoRedoHandlerService).toSelf().inSingletonScope(); + bindContributionProvider(bind, UndoRedoHandler); + bind(DomInputUndoRedoHandler).toSelf().inSingletonScope(); + bind(UndoRedoHandler).toService(DomInputUndoRedoHandler); }); diff --git a/packages/core/src/browser/index.ts b/packages/core/src/browser/index.ts index 42277fc2f7833..ecc1ccc4da2f8 100644 --- a/packages/core/src/browser/index.ts +++ b/packages/core/src/browser/index.ts @@ -47,3 +47,4 @@ export * from './decoration-style'; export * from './styling-service'; export * from './hover-service'; export * from './saveable-service'; +export * from './undo-redo-handler'; diff --git a/packages/core/src/browser/undo-redo-handler.ts b/packages/core/src/browser/undo-redo-handler.ts new file mode 100644 index 0000000000000..180ab9098d678 --- /dev/null +++ b/packages/core/src/browser/undo-redo-handler.ts @@ -0,0 +1,85 @@ +// ***************************************************************************** +// Copyright (C) 2024 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { inject, injectable, named, postConstruct } from 'inversify'; +import { ContributionProvider } from '../common'; + +export const UndoRedoHandler = Symbol('UndoRedoHandler'); + +export interface UndoRedoHandler { + priority: number; + select(): T | undefined; + undo(item: T): void; + redo(item: T): void; +} + +@injectable() +export class UndoRedoHandlerService { + + @inject(ContributionProvider) @named(UndoRedoHandler) + protected readonly provider: ContributionProvider>; + + protected handlers: UndoRedoHandler[]; + + @postConstruct() + protected init(): void { + this.handlers = this.provider.getContributions().sort((a, b) => b.priority - a.priority); + } + + undo(): void { + for (const handler of this.handlers) { + const selection = handler.select(); + if (selection) { + handler.undo(selection); + return; + } + } + } + + redo(): void { + for (const handler of this.handlers) { + const selection = handler.select(); + if (selection) { + handler.redo(selection); + return; + } + } + } + +} + +@injectable() +export class DomInputUndoRedoHandler implements UndoRedoHandler { + + priority = 1000; + + select(): Element | undefined { + const element = document.activeElement; + if (element && ['input', 'textarea'].includes(element.tagName.toLowerCase())) { + return element; + } + return undefined; + } + + undo(item: Element): void { + document.execCommand('undo'); + } + + redo(item: Element): void { + document.execCommand('redo'); + } + +} diff --git a/packages/monaco/src/browser/monaco-command.ts b/packages/monaco/src/browser/monaco-command.ts index fef9775b2d779..0b194b4397a6f 100644 --- a/packages/monaco/src/browser/monaco-command.ts +++ b/packages/monaco/src/browser/monaco-command.ts @@ -34,8 +34,6 @@ import { ICodeEditorService } from '@theia/monaco-editor-core/esm/vs/editor/brow export namespace MonacoCommands { export const COMMON_ACTIONS = new Map([ - ['undo', CommonCommands.UNDO.id], - ['redo', CommonCommands.REDO.id], ['editor.action.selectAll', CommonCommands.SELECT_ALL.id], ['actions.find', CommonCommands.FIND.id], ['editor.action.startFindReplaceAction', CommonCommands.REPLACE.id], @@ -47,7 +45,9 @@ export namespace MonacoCommands { export const GO_TO_DEFINITION = 'editor.action.revealDefinition'; export const EXCLUDE_ACTIONS = new Set([ - 'editor.action.quickCommand' + 'editor.action.quickCommand', + 'undo', + 'redo' ]); } diff --git a/packages/monaco/src/browser/monaco-editor-model.ts b/packages/monaco/src/browser/monaco-editor-model.ts index 7d05e457d0d02..b1ebdbaffe75f 100644 --- a/packages/monaco/src/browser/monaco-editor-model.ts +++ b/packages/monaco/src/browser/monaco-editor-model.ts @@ -113,6 +113,14 @@ export class MonacoEditorModel implements IResolvedTextEditorModel, TextEditorDo ); } + undo(): void { + this.model.undo(); + } + + redo(): void { + this.model.redo(); + } + dispose(): void { this.toDispose.dispose(); } diff --git a/packages/monaco/src/browser/monaco-frontend-module.ts b/packages/monaco/src/browser/monaco-frontend-module.ts index 0fc9b9ffaf317..0bd3a74f2500c 100644 --- a/packages/monaco/src/browser/monaco-frontend-module.ts +++ b/packages/monaco/src/browser/monaco-frontend-module.ts @@ -20,7 +20,8 @@ import { MenuContribution, CommandContribution, quickInputServicePath } from '@t import { FrontendApplicationContribution, KeybindingContribution, PreferenceService, PreferenceSchemaProvider, createPreferenceProxy, - PreferenceScope, PreferenceChange, OVERRIDE_PROPERTY_PATTERN, QuickInputService, StylingParticipant, WebSocketConnectionProvider + PreferenceScope, PreferenceChange, OVERRIDE_PROPERTY_PATTERN, QuickInputService, StylingParticipant, WebSocketConnectionProvider, + UndoRedoHandler } from '@theia/core/lib/browser'; import { TextEditorProvider, DiffNavigatorProvider, TextEditor } from '@theia/editor/lib/browser'; import { MonacoEditorProvider, MonacoEditorFactory } from './monaco-editor-provider'; @@ -73,6 +74,7 @@ import { ThemeService } from '@theia/core/lib/browser/theming'; import { ThemeServiceWithDB } from './monaco-indexed-db'; import { IContextKeyService } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/common/contextkey'; import { IThemeService } from '@theia/monaco-editor-core/esm/vs/platform/theme/common/themeService'; +import { ActiveMonacoUndoRedoHandler, FocusedMonacoUndoRedoHandler } from './monaco-undo-redo-handler'; export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(MonacoThemingService).toSelf().inSingletonScope(); @@ -175,6 +177,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(MonacoIconRegistry).toSelf().inSingletonScope(); bind(IconRegistry).toService(MonacoIconRegistry); + + bind(FocusedMonacoUndoRedoHandler).toSelf().inSingletonScope(); + bind(ActiveMonacoUndoRedoHandler).toSelf().inSingletonScope(); + bind(UndoRedoHandler).toService(FocusedMonacoUndoRedoHandler); + bind(UndoRedoHandler).toService(ActiveMonacoUndoRedoHandler); }); export const MonacoConfigurationService = Symbol('MonacoConfigurationService'); diff --git a/packages/monaco/src/browser/monaco-undo-redo-handler.ts b/packages/monaco/src/browser/monaco-undo-redo-handler.ts new file mode 100644 index 0000000000000..51a5e51c1069d --- /dev/null +++ b/packages/monaco/src/browser/monaco-undo-redo-handler.ts @@ -0,0 +1,64 @@ +// ***************************************************************************** +// Copyright (C) 2024 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { UndoRedoHandler } from '@theia/core/lib/browser'; +import { injectable } from '@theia/core/shared/inversify'; +import { ICodeEditor } from '@theia/monaco-editor-core/esm/vs/editor/browser/editorBrowser'; +import { ICodeEditorService } from '@theia/monaco-editor-core/esm/vs/editor/browser/services/codeEditorService'; +import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices'; + +@injectable() +export abstract class AbstractMonacoUndoRedoHandler implements UndoRedoHandler { + priority: number; + abstract select(): ICodeEditor | undefined; + undo(item: ICodeEditor): void { + item.trigger('MonacoUndoRedoHandler', 'undo', undefined); + } + redo(item: ICodeEditor): void { + item.trigger('MonacoUndoRedoHandler', 'redo', undefined); + } +} + +@injectable() +export class FocusedMonacoUndoRedoHandler extends AbstractMonacoUndoRedoHandler { + override priority = 10000; + + protected codeEditorService = StandaloneServices.get(ICodeEditorService); + + override select(): ICodeEditor | undefined { + const focusedEditor = this.codeEditorService.getFocusedCodeEditor(); + if (focusedEditor && focusedEditor.hasTextFocus()) { + return focusedEditor; + } + return undefined; + } +} + +@injectable() +export class ActiveMonacoUndoRedoHandler extends AbstractMonacoUndoRedoHandler { + override priority = 0; + + protected codeEditorService = StandaloneServices.get(ICodeEditorService); + + override select(): ICodeEditor | undefined { + const focusedEditor = this.codeEditorService.getActiveCodeEditor(); + if (focusedEditor) { + focusedEditor.focus(); + return focusedEditor; + } + return undefined; + } +} diff --git a/packages/notebook/src/browser/contributions/notebook-actions-contribution.ts b/packages/notebook/src/browser/contributions/notebook-actions-contribution.ts index da1bcc065397f..3fce82523ed1d 100644 --- a/packages/notebook/src/browser/contributions/notebook-actions-contribution.ts +++ b/packages/notebook/src/browser/contributions/notebook-actions-contribution.ts @@ -16,13 +16,12 @@ import { Command, CommandContribution, CommandHandler, CommandRegistry, CompoundMenuNodeRole, MenuContribution, MenuModelRegistry, nls, URI } from '@theia/core'; import { inject, injectable } from '@theia/core/shared/inversify'; -import { ApplicationShell, codicon, CommonCommands, KeybindingContribution, KeybindingRegistry } from '@theia/core/lib/browser'; +import { ApplicationShell, codicon, KeybindingContribution, KeybindingRegistry } from '@theia/core/lib/browser'; import { NotebookModel } from '../view-model/notebook-model'; import { NotebookService } from '../service/notebook-service'; import { CellEditType, CellKind, NotebookCommand } from '../../common'; import { NotebookKernelQuickPickService } from '../service/notebook-kernel-quick-pick-service'; import { NotebookExecutionService } from '../service/notebook-execution-service'; -import { NotebookEditorWidget } from '../notebook-editor-widget'; import { NotebookEditorWidgetService } from '../service/notebook-editor-widget-service'; import { NOTEBOOK_CELL_CURSOR_FIRST_LINE, NOTEBOOK_CELL_CURSOR_LAST_LINE, NOTEBOOK_CELL_FOCUSED, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_HAS_OUTPUTS } from './notebook-context-keys'; import { NotebookClipboardService } from '../service/notebook-clipboard-service'; @@ -208,21 +207,6 @@ export class NotebookActionsContribution implements CommandContribution, MenuCon } ); - commands.registerHandler(CommonCommands.UNDO.id, { - isEnabled: () => { - const widget = this.shell.activeWidget; - return widget instanceof NotebookEditorWidget && !Boolean(widget.model?.readOnly); - }, - execute: () => (this.shell.activeWidget as NotebookEditorWidget).undo() - }); - commands.registerHandler(CommonCommands.REDO.id, { - isEnabled: () => { - const widget = this.shell.activeWidget; - return widget instanceof NotebookEditorWidget && !Boolean(widget.model?.readOnly); - }, - execute: () => (this.shell.activeWidget as NotebookEditorWidget).redo() - }); - commands.registerCommand(NotebookCommands.CUT_SELECTED_CELL, this.editableCommandHandler( () => { const model = this.notebookEditorWidgetService.focusedEditor?.model; diff --git a/packages/notebook/src/browser/contributions/notebook-undo-redo-handler.ts b/packages/notebook/src/browser/contributions/notebook-undo-redo-handler.ts new file mode 100644 index 0000000000000..367dae772664d --- /dev/null +++ b/packages/notebook/src/browser/contributions/notebook-undo-redo-handler.ts @@ -0,0 +1,41 @@ +// ***************************************************************************** +// Copyright (C) 2024 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { inject, injectable } from '@theia/core/shared/inversify'; +import { ApplicationShell, UndoRedoHandler } from '@theia/core/lib/browser'; +import { NotebookEditorWidget } from '../notebook-editor-widget'; + +@injectable() +export class NotebookUndoRedoHandler implements UndoRedoHandler { + + @inject(ApplicationShell) + protected readonly applicationShell: ApplicationShell; + + priority = 200; + select(): NotebookEditorWidget | undefined { + const current = this.applicationShell.currentWidget; + if (current instanceof NotebookEditorWidget) { + return current; + } + return undefined; + } + undo(item: NotebookEditorWidget): void { + item.undo(); + } + redo(item: NotebookEditorWidget): void { + item.redo(); + } +} diff --git a/packages/notebook/src/browser/notebook-frontend-module.ts b/packages/notebook/src/browser/notebook-frontend-module.ts index fda9e983c0d8b..b39d914333a6e 100644 --- a/packages/notebook/src/browser/notebook-frontend-module.ts +++ b/packages/notebook/src/browser/notebook-frontend-module.ts @@ -16,7 +16,7 @@ import '../../src/browser/style/index.css'; import { ContainerModule } from '@theia/core/shared/inversify'; -import { FrontendApplicationContribution, KeybindingContribution, LabelProviderContribution, OpenHandler, WidgetFactory } from '@theia/core/lib/browser'; +import { FrontendApplicationContribution, KeybindingContribution, LabelProviderContribution, OpenHandler, UndoRedoHandler, WidgetFactory } from '@theia/core/lib/browser'; import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution'; import { NotebookOpenHandler } from './notebook-open-handler'; import { CommandContribution, MenuContribution, ResourceResolver, } from '@theia/core'; @@ -46,6 +46,7 @@ import { NotebookOutputActionContribution } from './contributions/notebook-outpu import { NotebookClipboardService } from './service/notebook-clipboard-service'; import { bindNotebookPreferences } from './contributions/notebook-preferences'; import { NotebookOptionsService } from './service/notebook-options'; +import { NotebookUndoRedoHandler } from './contributions/notebook-undo-redo-handler'; export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(NotebookColorContribution).toSelf().inSingletonScope(); @@ -108,4 +109,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bindNotebookPreferences(bind); bind(NotebookOptionsService).toSelf().inSingletonScope(); + + bind(NotebookUndoRedoHandler).toSelf().inSingletonScope(); + bind(UndoRedoHandler).toService(NotebookUndoRedoHandler); }); diff --git a/packages/notebook/src/browser/style/index.css b/packages/notebook/src/browser/style/index.css index f2d45fdfd4d68..844629eb2b36d 100644 --- a/packages/notebook/src/browser/style/index.css +++ b/packages/notebook/src/browser/style/index.css @@ -62,17 +62,22 @@ width: calc(100% - 15px); } +/* Rendered Markdown Content */ + .theia-notebook-markdown-content { padding: 8px 16px 8px 36px; font-size: var(--theia-notebook-markdown-size); } +.theia-notebook-markdown-content>* { + font-weight: 400; +} + .theia-notebook-markdown-content>*:first-child { margin-top: 0; padding-top: 0; } -.theia-notebook-markdown-content>*:only-child, .theia-notebook-markdown-content>*:last-child { margin-bottom: 0; padding-bottom: 0; diff --git a/packages/notebook/src/browser/view-model/notebook-cell-model.ts b/packages/notebook/src/browser/view-model/notebook-cell-model.ts index 8272795d6f124..c6ed48582fc31 100644 --- a/packages/notebook/src/browser/view-model/notebook-cell-model.ts +++ b/packages/notebook/src/browser/view-model/notebook-cell-model.ts @@ -88,22 +88,22 @@ export interface NotebookCellModelProps { export class NotebookCellModel implements NotebookCell, Disposable { protected readonly onDidChangeOutputsEmitter = new Emitter(); - readonly onDidChangeOutputs: Event = this.onDidChangeOutputsEmitter.event; + readonly onDidChangeOutputs = this.onDidChangeOutputsEmitter.event; protected readonly onDidChangeOutputItemsEmitter = new Emitter(); - readonly onDidChangeOutputItems: Event = this.onDidChangeOutputItemsEmitter.event; + readonly onDidChangeOutputItems = this.onDidChangeOutputItemsEmitter.event; protected readonly onDidChangeContentEmitter = new Emitter<'content' | 'language' | 'mime'>(); - readonly onDidChangeContent: Event<'content' | 'language' | 'mime'> = this.onDidChangeContentEmitter.event; + readonly onDidChangeContent = this.onDidChangeContentEmitter.event; protected readonly onDidChangeMetadataEmitter = new Emitter(); - readonly onDidChangeMetadata: Event = this.onDidChangeMetadataEmitter.event; + readonly onDidChangeMetadata = this.onDidChangeMetadataEmitter.event; protected readonly onDidChangeInternalMetadataEmitter = new Emitter(); - readonly onDidChangeInternalMetadata: Event = this.onDidChangeInternalMetadataEmitter.event; + readonly onDidChangeInternalMetadata = this.onDidChangeInternalMetadataEmitter.event; protected readonly onDidChangeLanguageEmitter = new Emitter(); - readonly onDidChangeLanguage: Event = this.onDidChangeLanguageEmitter.event; + readonly onDidChangeLanguage = this.onDidChangeLanguageEmitter.event; protected readonly onDidRequestCellEditChangeEmitter = new Emitter(); readonly onDidRequestCellEditChange = this.onDidRequestCellEditChangeEmitter.event; @@ -115,10 +115,10 @@ export class NotebookCellModel implements NotebookCell, Disposable { readonly onWillBlurCellEditor = this.onWillBlurCellEditorEmitter.event; protected readonly onDidChangeEditorOptionsEmitter = new Emitter(); - readonly onDidChangeEditorOptions: Event = this.onDidChangeEditorOptionsEmitter.event; + readonly onDidChangeEditorOptions = this.onDidChangeEditorOptionsEmitter.event; protected readonly outputVisibilityChangeEmitter = new Emitter(); - readonly onDidChangeOutputVisibility: Event = this.outputVisibilityChangeEmitter.event; + readonly onDidChangeOutputVisibility = this.outputVisibilityChangeEmitter.event; protected readonly onDidFindMatchesEmitter = new Emitter(); readonly onDidFindMatches: Event = this.onDidFindMatchesEmitter.event; @@ -183,10 +183,12 @@ export class NotebookCellModel implements NotebookCell, Disposable { get source(): string { return this.props.source; } + set source(source: string) { this.props.source = source; this.textModel?.textEditorModel.setValue(source); } + get language(): string { return this.props.language; } diff --git a/packages/notebook/src/browser/view-model/notebook-model.ts b/packages/notebook/src/browser/view-model/notebook-model.ts index e60b5f0ee2fff..78faa0cdaeeda 100644 --- a/packages/notebook/src/browser/view-model/notebook-model.ts +++ b/packages/notebook/src/browser/view-model/notebook-model.ts @@ -246,7 +246,7 @@ export class NotebookModel implements Saveable, Disposable { setData(data: NotebookData, markDirty = true): void { // Replace all cells in the model this.dirtyCells = []; - this.replaceCells(0, this.cells.length, data.cells, false); + this.replaceCells(0, this.cells.length, data.cells, false, false); this.metadata = data.metadata; this.dirty = markDirty; this.onDidChangeContentEmitter.fire(); @@ -260,13 +260,15 @@ export class NotebookModel implements Saveable, Disposable { } undo(): void { - // TODO we probably need to check if a monaco editor is focused and if so, not undo - this.undoRedoService.undo(this.uri); + if (!this.readOnly) { + this.undoRedoService.undo(this.uri); + } } redo(): void { - // TODO see undo - this.undoRedoService.redo(this.uri); + if (!this.readOnly) { + this.undoRedoService.redo(this.uri); + } } setSelectedCell(cell: NotebookCellModel, scrollIntoView?: boolean): void { @@ -315,7 +317,7 @@ export class NotebookModel implements Saveable, Disposable { let scrollIntoView = true; switch (edit.editType) { case CellEditType.Replace: - this.replaceCells(edit.index, edit.count, edit.cells, computeUndoRedo); + this.replaceCells(edit.index, edit.count, edit.cells, computeUndoRedo, true); scrollIntoView = edit.cells.length > 0; break; case CellEditType.Output: { @@ -338,10 +340,10 @@ export class NotebookModel implements Saveable, Disposable { break; case CellEditType.Metadata: - this.changeCellMetadata(this.cells[cellIndex], edit.metadata, computeUndoRedo); + this.changeCellMetadata(this.cells[cellIndex], edit.metadata, false); break; case CellEditType.PartialMetadata: - this.changeCellMetadataPartial(this.cells[cellIndex], edit.metadata, computeUndoRedo); + this.changeCellMetadataPartial(this.cells[cellIndex], edit.metadata, false); break; case CellEditType.PartialInternalMetadata: this.changeCellInternalMetadataPartial(this.cells[cellIndex], edit.internalMetadata); @@ -350,7 +352,7 @@ export class NotebookModel implements Saveable, Disposable { this.changeCellLanguage(this.cells[cellIndex], edit.language, computeUndoRedo); break; case CellEditType.DocumentMetadata: - this.updateNotebookMetadata(edit.metadata, computeUndoRedo); + this.updateNotebookMetadata(edit.metadata, false); break; case CellEditType.Move: this.moveCellToIndex(cellIndex, edit.length, edit.newIdx, computeUndoRedo); @@ -363,11 +365,15 @@ export class NotebookModel implements Saveable, Disposable { } } + this.fireContentChange(); + } + + protected fireContentChange(): void { this.onDidChangeContentEmitter.fire(); this.onContentChangedEmitter.fire(); } - protected replaceCells(start: number, deleteCount: number, newCells: CellData[], computeUndoRedo: boolean): void { + protected replaceCells(start: number, deleteCount: number, newCells: CellData[], computeUndoRedo: boolean, requestEdit: boolean): void { const cells = newCells.map(cell => { const handle = this.nextHandle++; return this.cellModelFactory({ @@ -394,13 +400,20 @@ export class NotebookModel implements Saveable, Disposable { if (computeUndoRedo) { this.undoRedoService.pushElement(this.uri, - async () => this.replaceCells(start, newCells.length, deletedCells.map(cell => cell.getData()), false), - async () => this.replaceCells(start, deleteCount, newCells, false)); + async () => { + this.replaceCells(start, newCells.length, deletedCells.map(cell => cell.getData()), false, false); + this.fireContentChange(); + }, + async () => { + this.replaceCells(start, deleteCount, newCells, false, false); + this.fireContentChange(); + } + ); } this.onDidAddOrRemoveCellEmitter.fire({ rawEvent: { kind: NotebookCellsChangeType.ModelChange, changes }, newCellIds: cells.map(cell => cell.handle) }); this.onDidChangeContentEmitter.queue({ kind: NotebookCellsChangeType.ModelChange, changes }); - if (cells.length > 0) { + if (cells.length > 0 && requestEdit) { this.setSelectedCell(cells[cells.length - 1]); cells[cells.length - 1].requestEdit(); } @@ -478,8 +491,14 @@ export class NotebookModel implements Saveable, Disposable { protected moveCellToIndex(fromIndex: number, length: number, toIndex: number, computeUndoRedo: boolean): boolean { if (computeUndoRedo) { this.undoRedoService.pushElement(this.uri, - async () => { this.moveCellToIndex(toIndex, length, fromIndex, false); }, - async () => { this.moveCellToIndex(fromIndex, length, toIndex, false); } + async () => { + this.moveCellToIndex(toIndex, length, fromIndex, false); + this.fireContentChange(); + }, + async () => { + this.moveCellToIndex(fromIndex, length, toIndex, false); + this.fireContentChange(); + } ); } @@ -495,11 +514,9 @@ export class NotebookModel implements Saveable, Disposable { } protected isCellMetadataChanged(a: NotebookCellMetadata, b: NotebookCellMetadata): boolean { - const keys = new Set([...Object.keys(a || {}), ...Object.keys(b || {})]); + const keys = new Set([...Object.keys(a || {}), ...Object.keys(b || {})]); for (const key of keys) { - if ( - (a[key as keyof NotebookCellMetadata] !== b[key as keyof NotebookCellMetadata]) - ) { + if (a[key] !== b[key]) { return true; } } diff --git a/packages/notebook/src/browser/view/notebook-cell-editor.tsx b/packages/notebook/src/browser/view/notebook-cell-editor.tsx index de920fc66ee8e..969c031dc6753 100644 --- a/packages/notebook/src/browser/view/notebook-cell-editor.tsx +++ b/packages/notebook/src/browser/view/notebook-cell-editor.tsx @@ -90,7 +90,7 @@ export class CellEditor extends React.Component { this.toDispose.push(this.props.cell.onWillFocusCellEditor(focusRequest => { this.editor?.getControl().focus(); const lineCount = this.editor?.getControl().getModel()?.getLineCount(); - if (focusRequest && lineCount) { + if (focusRequest && lineCount !== undefined) { this.editor?.getControl().setPosition(focusRequest === 'lastLine' ? { lineNumber: lineCount, column: 1 } : { lineNumber: focusRequest, column: 1 }, diff --git a/packages/notebook/src/browser/view/notebook-cell-list-view.tsx b/packages/notebook/src/browser/view/notebook-cell-list-view.tsx index a9ecbfc3e3a0f..41b719be09bcb 100644 --- a/packages/notebook/src/browser/view/notebook-cell-list-view.tsx +++ b/packages/notebook/src/browser/view/notebook-cell-list-view.tsx @@ -14,7 +14,7 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** import * as React from '@theia/core/shared/react'; -import { CellEditType, CellKind } from '../../common'; +import { CellEditType, CellKind, NotebookCellsChangeType } from '../../common'; import { NotebookCellModel } from '../view-model/notebook-cell-model'; import { NotebookModel } from '../view-model/notebook-model'; import { NotebookCellToolbarFactory } from './notebook-cell-toolbar-factory'; @@ -68,6 +68,13 @@ export class NotebookCellListView extends React.Component { + if (events.some(e => e.kind === NotebookCellsChangeType.Move)) { + // When a cell has been moved, we need to rerender the whole component + this.forceUpdate(); + } + })); + this.toDispose.push(props.notebookModel.onDidChangeSelectedCell(e => { this.setState({ ...this.state, diff --git a/packages/notebook/src/browser/view/notebook-markdown-cell-view.tsx b/packages/notebook/src/browser/view/notebook-markdown-cell-view.tsx index 65c96082f9325..04a835dbf67df 100644 --- a/packages/notebook/src/browser/view/notebook-markdown-cell-view.tsx +++ b/packages/notebook/src/browser/view/notebook-markdown-cell-view.tsx @@ -100,31 +100,31 @@ function MarkdownCell({ } return searchInMarkdown(instance, options); }; - const selectListener = cell.onDidSelectFindMatch(match => { - markdownContent.scrollIntoView({ - behavior: 'instant', - block: 'center', - }); - }); return () => { - selectListener.dispose(); cell.onMarkdownFind = undefined; instance.unmark(); }; } }, [editMode, cell.source]); - let markdownContent: HTMLElement = React.useMemo(() => { + let markdownContent: HTMLElement[] = React.useMemo(() => { const markdownString = new MarkdownStringImpl(cell.source, { supportHtml: true, isTrusted: true }); - return markdownRenderer.render(markdownString).element; + const rendered = markdownRenderer.render(markdownString).element; + const children: HTMLElement[] = []; + rendered.childNodes.forEach(child => { + if (child instanceof HTMLElement) { + children.push(child); + } + }); + return children; }, [cell.source]); - if (!markdownContent.hasChildNodes()) { + if (markdownContent.length === 0) { const italic = document.createElement('i'); italic.className = 'theia-notebook-empty-markdown'; italic.innerText = nls.localizeByDefault('Empty markdown cell, double-click or press enter to edit.'); italic.style.pointerEvents = 'none'; - markdownContent = italic; + markdownContent = [italic]; empty = true; } @@ -140,7 +140,7 @@ function MarkdownCell({ ) : (
cell.requestEdit()} - ref={node => node?.replaceChildren(markdownContent)} + ref={node => node?.replaceChildren(...markdownContent)} />); } diff --git a/packages/plugin-ext/src/main/browser/custom-editors/custom-editor-contribution.ts b/packages/plugin-ext/src/main/browser/custom-editors/custom-editor-contribution.ts deleted file mode 100644 index 485cdaa697eb4..0000000000000 --- a/packages/plugin-ext/src/main/browser/custom-editors/custom-editor-contribution.ts +++ /dev/null @@ -1,38 +0,0 @@ -// ***************************************************************************** -// Copyright (C) 2021 SAP SE or an SAP affiliate company and others. -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License v. 2.0 which is available at -// http://www.eclipse.org/legal/epl-2.0. -// -// This Source Code may also be made available under the following Secondary -// Licenses when the conditions for such availability set forth in the Eclipse -// Public License v. 2.0 are satisfied: GNU General Public License, version 2 -// with the GNU Classpath Exception which is available at -// https://www.gnu.org/software/classpath/license.html. -// -// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 -// ***************************************************************************** - -import { injectable, inject } from '@theia/core/shared/inversify'; -import { CommandRegistry, CommandContribution } from '@theia/core/lib/common'; -import { ApplicationShell, CommonCommands } from '@theia/core/lib/browser'; -import { CustomEditorWidget } from './custom-editor-widget'; - -@injectable() -export class CustomEditorContribution implements CommandContribution { - - @inject(ApplicationShell) - protected readonly shell: ApplicationShell; - - registerCommands(commands: CommandRegistry): void { - commands.registerHandler(CommonCommands.UNDO.id, { - isEnabled: () => this.shell.activeWidget instanceof CustomEditorWidget, - execute: () => (this.shell.activeWidget as CustomEditorWidget).undo() - }); - commands.registerHandler(CommonCommands.REDO.id, { - isEnabled: () => this.shell.activeWidget instanceof CustomEditorWidget, - execute: () => (this.shell.activeWidget as CustomEditorWidget).redo() - }); - } -} diff --git a/packages/plugin-ext/src/main/browser/custom-editors/custom-editor-undo-redo-handler.ts b/packages/plugin-ext/src/main/browser/custom-editors/custom-editor-undo-redo-handler.ts new file mode 100644 index 0000000000000..328433ad36aec --- /dev/null +++ b/packages/plugin-ext/src/main/browser/custom-editors/custom-editor-undo-redo-handler.ts @@ -0,0 +1,41 @@ +// ***************************************************************************** +// Copyright (C) 2024 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { inject, injectable } from '@theia/core/shared/inversify'; +import { ApplicationShell, UndoRedoHandler } from '@theia/core/lib/browser'; +import { CustomEditorWidget } from './custom-editor-widget'; + +@injectable() +export class CustomEditorUndoRedoHandler implements UndoRedoHandler { + + @inject(ApplicationShell) + protected readonly applicationShell: ApplicationShell; + + priority = 190; + select(): CustomEditorWidget | undefined { + const current = this.applicationShell.currentWidget; + if (current instanceof CustomEditorWidget) { + return current; + } + return undefined; + } + undo(item: CustomEditorWidget): void { + item.undo(); + } + redo(item: CustomEditorWidget): void { + item.redo(); + } +} diff --git a/packages/plugin-ext/src/main/browser/custom-editors/custom-editor-widget.ts b/packages/plugin-ext/src/main/browser/custom-editors/custom-editor-widget.ts index 7c626f3b69357..7d277a44565b5 100644 --- a/packages/plugin-ext/src/main/browser/custom-editors/custom-editor-widget.ts +++ b/packages/plugin-ext/src/main/browser/custom-editors/custom-editor-widget.ts @@ -21,7 +21,6 @@ import { ApplicationShell, NavigatableWidget, Saveable, SaveableSource, SaveOpti import { SaveableService } from '@theia/core/lib/browser/saveable-service'; import { Reference } from '@theia/core/lib/common/reference'; import { WebviewWidget } from '../webview/webview'; -import { UndoRedoService } from '@theia/editor/lib/browser/undo-redo-service'; import { CustomEditorModel } from './custom-editors-main'; @injectable() @@ -43,9 +42,6 @@ export class CustomEditorWidget extends WebviewWidget implements SaveableSource, return this._modelRef.object; } - @inject(UndoRedoService) - protected readonly undoRedoService: UndoRedoService; - @inject(ApplicationShell) protected readonly shell: ApplicationShell; @@ -64,11 +60,11 @@ export class CustomEditorWidget extends WebviewWidget implements SaveableSource, } undo(): void { - this.undoRedoService.undo(this.resource); + this._modelRef.object.undo(); } redo(): void { - this.undoRedoService.redo(this.resource); + this._modelRef.object.redo(); } async save(options?: SaveOptions): Promise { diff --git a/packages/plugin-ext/src/main/browser/custom-editors/custom-editors-main.ts b/packages/plugin-ext/src/main/browser/custom-editors/custom-editors-main.ts index 4368a500f1e69..95e0dcad4ba39 100644 --- a/packages/plugin-ext/src/main/browser/custom-editors/custom-editors-main.ts +++ b/packages/plugin-ext/src/main/browser/custom-editors/custom-editors-main.ts @@ -284,6 +284,9 @@ export interface CustomEditorModel extends Saveable, Disposable { revert(options?: Saveable.RevertOptions): Promise; saveCustomEditor(options?: SaveOptions): Promise; saveCustomEditorAs(resource: TheiaURI, targetResource: TheiaURI, options?: SaveOptions): Promise; + + undo(): void; + redo(): void; } export class MainCustomEditorModel implements CustomEditorModel { @@ -436,7 +439,7 @@ export class MainCustomEditorModel implements CustomEditorModel { } } - private async undo(): Promise { + async undo(): Promise { if (!this.editable) { return; } @@ -453,7 +456,7 @@ export class MainCustomEditorModel implements CustomEditorModel { await this.proxy.$undo(this.resource, this.viewType, undoneEdit, this.dirty); } - private async redo(): Promise { + async redo(): Promise { if (!this.editable) { return; } @@ -571,4 +574,12 @@ export class CustomTextEditorModel implements CustomEditorModel { await this.saveCustomEditor(options); await this.fileService.copy(resource, targetResource, { overwrite: false }); } + + undo(): void { + this.editorTextModel.undo(); + } + + redo(): void { + this.editorTextModel.redo(); + } } diff --git a/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts b/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts index dc8d2bfdaf5c8..7323df220e868 100644 --- a/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts +++ b/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts @@ -21,7 +21,8 @@ import '../../../src/main/browser/style/comments.css'; import { ContainerModule } from '@theia/core/shared/inversify'; import { FrontendApplicationContribution, WidgetFactory, bindViewContribution, - ViewContainerIdentifier, ViewContainer, createTreeContainer, TreeWidget, LabelProviderContribution + ViewContainerIdentifier, ViewContainer, createTreeContainer, TreeWidget, LabelProviderContribution, + UndoRedoHandler } from '@theia/core/lib/browser'; import { MaybePromise, CommandContribution, ResourceResolver, bindContributionProvider } from '@theia/core/lib/common'; import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging'; @@ -66,7 +67,6 @@ import { CommentsService, PluginCommentService } from './comments/comments-servi import { CommentingRangeDecorator } from './comments/comments-decorator'; import { CommentsContribution } from './comments/comments-contribution'; import { CommentsContextKeyService } from './comments/comments-context-key-service'; -import { CustomEditorContribution } from './custom-editors/custom-editor-contribution'; import { PluginCustomEditorRegistry } from './custom-editors/plugin-custom-editor-registry'; import { CustomEditorWidgetFactory } from '../browser/custom-editors/custom-editor-widget-factory'; import { CustomEditorWidget } from './custom-editors/custom-editor-widget'; @@ -90,6 +90,7 @@ import { NotebookCellModel } from '@theia/notebook/lib/browser/view-model/notebo import { NotebookModel } from '@theia/notebook/lib/browser/view-model/notebook-model'; import { ArgumentProcessorContribution } from './command-registry-main'; import { WebviewSecondaryWindowSupport } from './webview/webview-secondary-window-support'; +import { CustomEditorUndoRedoHandler } from './custom-editors/custom-editor-undo-redo-handler'; export default new ContainerModule((bind, unbind, isBound, rebind) => { @@ -191,14 +192,13 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(FrontendApplicationContribution).toService(WebviewSecondaryWindowSupport); bind(FrontendApplicationContribution).toService(WebviewContextKeys); - bind(CustomEditorContribution).toSelf().inSingletonScope(); - bind(CommandContribution).toService(CustomEditorContribution); - bind(PluginCustomEditorRegistry).toSelf().inSingletonScope(); bind(CustomEditorService).toSelf().inSingletonScope(); bind(CustomEditorWidget).toSelf(); bind(CustomEditorWidgetFactory).toDynamicValue(ctx => new CustomEditorWidgetFactory(ctx.container)).inSingletonScope(); bind(WidgetFactory).toService(CustomEditorWidgetFactory); + bind(CustomEditorUndoRedoHandler).toSelf().inSingletonScope(); + bind(UndoRedoHandler).toService(CustomEditorUndoRedoHandler); bind(PluginViewWidget).toSelf(); bind(WidgetFactory).toDynamicValue(({ container }) => ({