diff --git a/.gitpod.yml b/.gitpod.yml index a03d38cc692cc..1deab0c9a8306 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -12,13 +12,10 @@ ports: - port: 9339 # Node.js debug port onOpen: ignore tasks: - - init: yarn --network-timeout 100000 && yarn build:examples && yarn download:plugins + - init: yarn --network-timeout 100000 && yarn browser build && yarn download:plugins command: > jwm & - yarn --cwd examples/browser start ../.. --hostname=0.0.0.0 -github: - prebuilds: - pullRequestsFromForks: true + yarn browser start ../.. --hostname=0.0.0.0 vscode: extensions: - - dbaeumer.vscode-eslint@2.0.0:CwAMx4wYz1Kq39+1Aul4VQ== + - dbaeumer.vscode-eslint diff --git a/examples/api-samples/src/browser/api-samples-frontend-module.ts b/examples/api-samples/src/browser/api-samples-frontend-module.ts index eae887e3fa360..fe6c53d05527c 100644 --- a/examples/api-samples/src/browser/api-samples-frontend-module.ts +++ b/examples/api-samples/src/browser/api-samples-frontend-module.ts @@ -29,6 +29,7 @@ import { bindMonacoPreferenceExtractor } from './monaco-editor-preferences/monac import { rebindOVSXClientFactory } from '../common/vsx/sample-ovsx-client-factory'; import { bindSampleAppInfo } from './vsx/sample-frontend-app-info'; import { bindTestSample } from './test/sample-test-contribution'; +import { bindSampleFileSystemCapabilitiesCommands } from './file-system/sample-file-system-capabilities'; export default new ContainerModule(( bind: interfaces.Bind, @@ -47,5 +48,6 @@ export default new ContainerModule(( bindMonacoPreferenceExtractor(bind); bindSampleAppInfo(bind); bindTestSample(bind); + bindSampleFileSystemCapabilitiesCommands(bind); rebindOVSXClientFactory(rebind); }); diff --git a/examples/api-samples/src/browser/file-system/sample-file-system-capabilities.ts b/examples/api-samples/src/browser/file-system/sample-file-system-capabilities.ts new file mode 100644 index 0000000000000..e5ebf2e8418ea --- /dev/null +++ b/examples/api-samples/src/browser/file-system/sample-file-system-capabilities.ts @@ -0,0 +1,48 @@ +/******************************************************************************** + * 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 { CommandContribution, CommandRegistry } from '@theia/core'; +import { inject, injectable, interfaces } from '@theia/core/shared/inversify'; +import { RemoteFileSystemProvider } from '@theia/filesystem/lib/common/remote-file-system-provider'; +import { FileSystemProviderCapabilities } from '@theia/filesystem/lib/common/files'; + +@injectable() +export class SampleFileSystemCapabilities implements CommandContribution { + + @inject(RemoteFileSystemProvider) + protected readonly remoteFileSystemProvider: RemoteFileSystemProvider; + + registerCommands(commands: CommandRegistry): void { + commands.registerCommand({ + id: 'toggleFileSystemReadonly', + label: 'Toggle File System Readonly' + }, { + execute: () => { + const readonly = (this.remoteFileSystemProvider.capabilities & FileSystemProviderCapabilities.Readonly) !== 0; + if (readonly) { + this.remoteFileSystemProvider['setCapabilities'](this.remoteFileSystemProvider.capabilities & ~FileSystemProviderCapabilities.Readonly); + } else { + this.remoteFileSystemProvider['setCapabilities'](this.remoteFileSystemProvider.capabilities | FileSystemProviderCapabilities.Readonly); + } + } + }); + } + +} + +export function bindSampleFileSystemCapabilitiesCommands(bind: interfaces.Bind): void { + bind(CommandContribution).to(SampleFileSystemCapabilities).inSingletonScope(); +} diff --git a/packages/core/src/browser/widgets/widget.ts b/packages/core/src/browser/widgets/widget.ts index 8ea9b5959e141..9a71b96074208 100644 --- a/packages/core/src/browser/widgets/widget.ts +++ b/packages/core/src/browser/widgets/widget.ts @@ -381,12 +381,20 @@ export function pin(title: Title): void { } } +export function isLocked(title: Title): boolean { + return title.className.includes(LOCKED_CLASS); +} + export function lock(title: Title): void { if (!title.className.includes(LOCKED_CLASS)) { title.className += ` ${LOCKED_CLASS}`; } } +export function unlock(title: Title): void { + title.className = title.className.replace(LOCKED_CLASS, '').trim(); +} + export function togglePinned(title?: Title): void { if (title) { if (isPinned(title)) { diff --git a/packages/core/src/common/resource.ts b/packages/core/src/common/resource.ts index 4c62063e94478..abe188733a925 100644 --- a/packages/core/src/common/resource.ts +++ b/packages/core/src/common/resource.ts @@ -25,6 +25,7 @@ import { CancellationToken } from './cancellation'; import { ApplicationError } from './application-error'; import { ReadableStream, Readable } from './stream'; import { SyncReferenceCollection, Reference } from './reference'; +import { MarkdownString } from './markdown-rendering'; export interface ResourceVersion { } @@ -55,7 +56,10 @@ export interface Resource extends Disposable { * Undefined if a resource did not read content yet. */ readonly encoding?: string | undefined; - readonly isReadonly?: boolean; + + readonly onDidChangeReadOnly?: Event; + + readonly readOnly?: boolean | MarkdownString; /** * Reads latest content of this resource. * diff --git a/packages/editor/src/browser/editor-widget.ts b/packages/editor/src/browser/editor-widget.ts index 708dba0994997..d31e089b09843 100644 --- a/packages/editor/src/browser/editor-widget.ts +++ b/packages/editor/src/browser/editor-widget.ts @@ -15,7 +15,7 @@ // ***************************************************************************** import { Disposable, SelectionService, Event, UNTITLED_SCHEME, DisposableCollection } from '@theia/core/lib/common'; -import { Widget, BaseWidget, Message, Saveable, SaveableSource, Navigatable, StatefulWidget, lock, TabBar, DockPanel } from '@theia/core/lib/browser'; +import { Widget, BaseWidget, Message, Saveable, SaveableSource, Navigatable, StatefulWidget, lock, TabBar, DockPanel, unlock } from '@theia/core/lib/browser'; import URI from '@theia/core/lib/common/uri'; import { find } from '@theia/core/shared/@phosphor/algorithm'; import { TextEditor } from './editor'; @@ -38,6 +38,13 @@ export class EditorWidget extends BaseWidget implements SaveableSource, Navigata this.toDispose.push(this.toDisposeOnTabbarChange); this.toDispose.push(this.editor.onSelectionChanged(() => this.setSelection())); this.toDispose.push(this.editor.onFocusChanged(() => this.setSelection())); + this.toDispose.push(this.editor.onDidChangeReadOnly(isReadonly => { + if (isReadonly) { + lock(this.title); + } else { + unlock(this.title); + } + })); this.toDispose.push(Disposable.create(() => { if (this.selectionService.selection === this.editor) { this.selectionService.selection = undefined; diff --git a/packages/editor/src/browser/editor.ts b/packages/editor/src/browser/editor.ts index 8a425e91970f6..ce792c7a6929e 100644 --- a/packages/editor/src/browser/editor.ts +++ b/packages/editor/src/browser/editor.ts @@ -20,6 +20,7 @@ import URI from '@theia/core/lib/common/uri'; import { Event, Disposable, TextDocumentContentChangeDelta, Reference, isObject } from '@theia/core/lib/common'; import { Saveable, Navigatable, Widget } from '@theia/core/lib/browser'; import { EditorDecoration } from './decorations/editor-decoration'; +import { MarkdownString } from '@theia/core/lib/common/markdown-rendering'; export { Position, Range, Location }; @@ -207,7 +208,8 @@ export interface TextEditor extends Disposable, TextEditorSelection, Navigatable readonly node: HTMLElement; readonly uri: URI; - readonly isReadonly: boolean; + readonly isReadonly: boolean | MarkdownString; + readonly onDidChangeReadOnly: Event; readonly document: TextEditorDocument; readonly onDocumentContentChanged: Event; diff --git a/packages/filesystem/src/browser/file-resource.ts b/packages/filesystem/src/browser/file-resource.ts index ef515c4a31ff3..bc8c69387d449 100644 --- a/packages/filesystem/src/browser/file-resource.ts +++ b/packages/filesystem/src/browser/file-resource.ts @@ -27,6 +27,7 @@ import { LabelProvider } from '@theia/core/lib/browser/label-provider'; import { GENERAL_MAX_FILE_SIZE_MB } from './filesystem-preferences'; import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; import { nls } from '@theia/core'; +import { MarkdownString } from '@theia/core/lib/common/markdown-rendering'; export interface FileResourceVersion extends ResourceVersion { readonly encoding: string; @@ -54,6 +55,9 @@ export class FileResource implements Resource { protected readonly onDidChangeContentsEmitter = new Emitter(); readonly onDidChangeContents: Event = this.onDidChangeContentsEmitter.event; + protected readonly onDidChangeReadOnlyEmitter = new Emitter(); + readonly onDidChangeReadOnly: Event = this.onDidChangeReadOnlyEmitter.event; + protected _version: FileResourceVersion | undefined; get version(): FileResourceVersion | undefined { return this._version; @@ -61,7 +65,7 @@ export class FileResource implements Resource { get encoding(): string | undefined { return this._version?.encoding; } - get isReadonly(): boolean { + get readOnly(): boolean { return this.options.isReadonly || this.fileService.hasCapability(this.uri, FileSystemProviderCapabilities.Readonly); } @@ -71,6 +75,7 @@ export class FileResource implements Resource { protected readonly options: FileResourceOptions ) { this.toDispose.push(this.onDidChangeContentsEmitter); + this.toDispose.push(this.onDidChangeReadOnlyEmitter); this.toDispose.push(this.fileService.onDidFilesChange(event => { if (event.contains(this.uri)) { this.sync(); @@ -220,17 +225,24 @@ export class FileResource implements Resource { saveContents?: Resource['saveContents']; saveContentChanges?: Resource['saveContentChanges']; protected updateSavingContentChanges(): void { - if (this.isReadonly) { + let changed = false; + if (this.readOnly) { + changed = Boolean(this.saveContents); delete this.saveContentChanges; delete this.saveContents; delete this.saveStream; } else { + changed = !Boolean(this.saveContents); this.saveContents = this.doWrite; this.saveStream = this.doWrite; if (this.fileService.hasCapability(this.uri, FileSystemProviderCapabilities.Update)) { this.saveContentChanges = this.doSaveContentChanges; } } + if (changed) { + // Only actually bother to call the event if the value has changed. + this.onDidChangeReadOnlyEmitter.fire(this.readOnly); + } } protected doSaveContentChanges: Resource['saveContentChanges'] = async (changes, options) => { const version = options?.version || this._version; diff --git a/packages/monaco/src/browser/monaco-editor-model.ts b/packages/monaco/src/browser/monaco-editor-model.ts index f2f44cd6776b8..32a4731254cab 100644 --- a/packages/monaco/src/browser/monaco-editor-model.ts +++ b/packages/monaco/src/browser/monaco-editor-model.ts @@ -32,6 +32,7 @@ import { ILanguageService } from '@theia/monaco-editor-core/esm/vs/editor/common import { IModelService } from '@theia/monaco-editor-core/esm/vs/editor/common/services/model'; import { createTextBufferFactoryFromStream } from '@theia/monaco-editor-core/esm/vs/editor/common/model/textModel'; import { editorGeneratedPreferenceProperties } from '@theia/editor/lib/browser/editor-generated-preference-schema'; +import { MarkdownString } from '@theia/core/lib/common/markdown-rendering'; export { TextDocumentSaveReason @@ -81,6 +82,8 @@ export class MonacoEditorModel implements IResolvedTextEditorModel, TextEditorDo protected readonly onDidChangeEncodingEmitter = new Emitter(); readonly onDidChangeEncoding = this.onDidChangeEncodingEmitter.event; + readonly onDidChangeReadOnly: Event = this.resource.onDidChangeReadOnly ?? Event.None; + private preferredEncoding: string | undefined; private contentEncoding: string | undefined; @@ -302,11 +305,11 @@ export class MonacoEditorModel implements IResolvedTextEditorModel, TextEditorDo return this.m2p.asRange(this.model.validateRange(this.p2m.asRange(range))); } - get readOnly(): boolean { - return this.resource.saveContents === undefined; + get readOnly(): boolean | MarkdownString { + return this.resource.readOnly ?? false; } - isReadonly(): boolean { + isReadonly(): boolean | MarkdownString { return this.readOnly; } diff --git a/packages/monaco/src/browser/monaco-editor-provider.ts b/packages/monaco/src/browser/monaco-editor-provider.ts index 0b928dc71ddd7..f2c6c0a65d3ba 100644 --- a/packages/monaco/src/browser/monaco-editor-provider.ts +++ b/packages/monaco/src/browser/monaco-editor-provider.ts @@ -26,7 +26,6 @@ import { MonacoDiffNavigatorFactory } from './monaco-diff-navigator-factory'; import { EditorServiceOverrides, MonacoEditor, MonacoEditorServices } from './monaco-editor'; import { MonacoEditorModel, WillSaveMonacoModelEvent } from './monaco-editor-model'; import { MonacoWorkspace } from './monaco-workspace'; -import { ApplicationServer } from '@theia/core/lib/common/application-protocol'; import { ContributionProvider } from '@theia/core'; import { KeybindingRegistry, OpenerService, open, WidgetOpenerOptions, FormatType } from '@theia/core/lib/browser'; import { MonacoResolvedKeybinding } from './monaco-resolved-keybinding'; @@ -86,8 +85,6 @@ export class MonacoEditorProvider { @inject(MonacoWorkspace) protected readonly workspace: MonacoWorkspace, @inject(EditorPreferences) protected readonly editorPreferences: EditorPreferences, @inject(MonacoDiffNavigatorFactory) protected readonly diffNavigatorFactory: MonacoDiffNavigatorFactory, - /** @deprecated since 1.6.0 */ - @inject(ApplicationServer) protected readonly applicationServer: ApplicationServer, ) { } diff --git a/packages/monaco/src/browser/monaco-editor.ts b/packages/monaco/src/browser/monaco-editor.ts index be9029fb2bde3..a01845db1df3f 100644 --- a/packages/monaco/src/browser/monaco-editor.ts +++ b/packages/monaco/src/browser/monaco-editor.ts @@ -50,6 +50,7 @@ import { IInstantiationService, ServiceIdentifier } from '@theia/monaco-editor-c import { ICodeEditor, IMouseTargetMargin } from '@theia/monaco-editor-core/esm/vs/editor/browser/editorBrowser'; import { IStandaloneEditorConstructionOptions, StandaloneEditor } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneCodeEditor'; import { ServiceCollection } from '@theia/monaco-editor-core/esm/vs/platform/instantiation/common/serviceCollection'; +import { MarkdownString } from '@theia/core/lib/common/markdown-rendering'; export type ServicePair = [ServiceIdentifier, T]; @@ -86,6 +87,7 @@ export class MonacoEditor extends MonacoEditorServices implements TextEditor { protected readonly onFocusChangedEmitter = new Emitter(); protected readonly onDocumentContentChangedEmitter = new Emitter(); protected readonly onMouseDownEmitter = new Emitter(); + readonly onDidChangeReadOnly = this.document.onDidChangeReadOnly; protected readonly onLanguageChangedEmitter = new Emitter(); readonly onLanguageChanged = this.onLanguageChangedEmitter.event; protected readonly onScrollChangedEmitter = new Emitter(); @@ -117,7 +119,10 @@ export class MonacoEditor extends MonacoEditorServices implements TextEditor { this.autoSizing = options && options.autoSizing !== undefined ? options.autoSizing : false; this.minHeight = options && options.minHeight !== undefined ? options.minHeight : -1; this.maxHeight = options && options.maxHeight !== undefined ? options.maxHeight : -1; - this.toDispose.push(this.create(options, override)); + this.toDispose.push(this.create({ + ...MonacoEditor.createReadOnlyOptions(document.readOnly), + ...options + }, override)); this.addHandlers(this.editor); } @@ -199,6 +204,9 @@ export class MonacoEditor extends MonacoEditorServices implements TextEditor { this.toDispose.push(codeEditor.onDidScrollChange(e => { this.onScrollChangedEmitter.fire(undefined); })); + this.toDispose.push(this.onDidChangeReadOnly(readOnly => { + codeEditor.updateOptions(MonacoEditor.createReadOnlyOptions(readOnly)); + })); } getVisibleRanges(): Range[] { @@ -221,8 +229,8 @@ export class MonacoEditor extends MonacoEditorServices implements TextEditor { return this.onDocumentContentChangedEmitter.event; } - get isReadonly(): boolean { - return this.document.isReadonly(); + get isReadonly(): boolean | MarkdownString { + return this.document.readOnly; } get cursor(): Position { @@ -642,4 +650,14 @@ export namespace MonacoEditor { return candidate && candidate.getControl() === control; }); } + + export function createReadOnlyOptions(readOnly?: boolean | MarkdownString): monaco.editor.IEditorOptions { + if (typeof readOnly === 'boolean') { + return { readOnly }; + } + if (readOnly) { + return { readOnly: true, readOnlyMessage: readOnly }; + } + return {}; + } } diff --git a/packages/monaco/src/browser/simple-monaco-editor.ts b/packages/monaco/src/browser/simple-monaco-editor.ts index eda4874ada840..de2a1d525f2e7 100644 --- a/packages/monaco/src/browser/simple-monaco-editor.ts +++ b/packages/monaco/src/browser/simple-monaco-editor.ts @@ -43,6 +43,7 @@ export class SimpleMonacoEditor extends MonacoEditorServices implements Disposab readonly onEncodingChanged = this.document.onDidChangeEncoding; protected readonly onResizeEmitter = new Emitter(); readonly onDidResize = this.onResizeEmitter.event; + readonly onDidChangeReadOnly = this.document.onDidChangeReadOnly; constructor( readonly uri: URI, @@ -62,7 +63,10 @@ export class SimpleMonacoEditor extends MonacoEditorServices implements Disposab this.onLanguageChangedEmitter, this.onScrollChangedEmitter ]); - this.toDispose.push(this.create(options, override)); + this.toDispose.push(this.create({ + ...MonacoEditor.createReadOnlyOptions(document.readOnly), + ...options + }, override)); this.addHandlers(this.editor); this.editor.setModel(document.textEditorModel); } @@ -125,6 +129,9 @@ export class SimpleMonacoEditor extends MonacoEditorServices implements Disposab this.toDispose.push(codeEditor.onDidScrollChange(e => { this.onScrollChangedEmitter.fire(undefined); })); + this.toDispose.push(this.onDidChangeReadOnly(readOnly => { + codeEditor.updateOptions(MonacoEditor.createReadOnlyOptions(readOnly)); + })); } setLanguage(languageId: string): void { diff --git a/packages/notebook/src/browser/contributions/notebook-actions-contribution.ts b/packages/notebook/src/browser/contributions/notebook-actions-contribution.ts index fcd1c94e4c141..4795d8da7b582 100644 --- a/packages/notebook/src/browser/contributions/notebook-actions-contribution.ts +++ b/packages/notebook/src/browser/contributions/notebook-actions-contribution.ts @@ -14,7 +14,7 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { Command, CommandContribution, CommandRegistry, CompoundMenuNodeRole, MenuContribution, MenuModelRegistry, nls } from '@theia/core'; +import { Command, CommandContribution, CommandHandler, CommandRegistry, CompoundMenuNodeRole, MenuContribution, MenuModelRegistry, nls } from '@theia/core'; import { inject, injectable } from '@theia/core/shared/inversify'; import { ApplicationShell, codicon, CommonCommands } from '@theia/core/lib/browser'; import { NotebookModel } from '../view-model/notebook-model'; @@ -100,37 +100,52 @@ export class NotebookActionsContribution implements CommandContribution, MenuCon } }); - commands.registerCommand(NotebookCommands.ADD_NEW_MARKDOWN_CELL_COMMAND, { - execute: (notebookModel: NotebookModel) => commands.executeCommand(NotebookCommands.ADD_NEW_CELL_COMMAND.id, notebookModel, CellKind.Markup) - }); + commands.registerCommand(NotebookCommands.ADD_NEW_MARKDOWN_CELL_COMMAND, this.editableCommandHandler( + notebookModel => commands.executeCommand(NotebookCommands.ADD_NEW_CELL_COMMAND.id, notebookModel, CellKind.Markup) + )); - commands.registerCommand(NotebookCommands.ADD_NEW_CODE_CELL_COMMAND, { - execute: (notebookModel: NotebookModel) => commands.executeCommand(NotebookCommands.ADD_NEW_CELL_COMMAND.id, notebookModel, CellKind.Code) - }); + commands.registerCommand(NotebookCommands.ADD_NEW_CODE_CELL_COMMAND, this.editableCommandHandler( + notebookModel => commands.executeCommand(NotebookCommands.ADD_NEW_CELL_COMMAND.id, notebookModel, CellKind.Code) + )); - commands.registerCommand(NotebookCommands.SELECT_KERNEL_COMMAND, { - execute: (notebookModel: NotebookModel) => this.notebookKernelQuickPickService.showQuickPick(notebookModel) - }); + commands.registerCommand(NotebookCommands.SELECT_KERNEL_COMMAND, this.editableCommandHandler( + notebookModel => this.notebookKernelQuickPickService.showQuickPick(notebookModel) + )); - commands.registerCommand(NotebookCommands.EXECUTE_NOTEBOOK_COMMAND, { - execute: (notebookModel: NotebookModel) => this.notebookExecutionService.executeNotebookCells(notebookModel, notebookModel.cells) - }); + commands.registerCommand(NotebookCommands.EXECUTE_NOTEBOOK_COMMAND, this.editableCommandHandler( + notebookModel => this.notebookExecutionService.executeNotebookCells(notebookModel, notebookModel.cells) + )); - commands.registerCommand(NotebookCommands.CLEAR_ALL_OUTPUTS_COMMAND, { - execute: (notebookModel: NotebookModel) => - notebookModel.cells.forEach(cell => cell.spliceNotebookCellOutputs({ start: 0, deleteCount: cell.outputs.length, newOutputs: [] })) - }); + commands.registerCommand(NotebookCommands.CLEAR_ALL_OUTPUTS_COMMAND, this.editableCommandHandler( + notebookModel => notebookModel.cells.forEach(cell => cell.spliceNotebookCellOutputs({ start: 0, deleteCount: cell.outputs.length, newOutputs: [] })) + )); commands.registerHandler(CommonCommands.UNDO.id, { - isEnabled: () => this.shell.activeWidget instanceof NotebookEditorWidget, + 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: () => this.shell.activeWidget instanceof NotebookEditorWidget, + isEnabled: () => { + const widget = this.shell.activeWidget; + return widget instanceof NotebookEditorWidget && !Boolean(widget.model?.readOnly); + }, execute: () => (this.shell.activeWidget as NotebookEditorWidget).redo() }); } + protected editableCommandHandler(execute: (notebookModel: NotebookModel) => void): CommandHandler { + return { + isEnabled: (notebookModel: NotebookModel) => !Boolean(notebookModel?.readOnly), + isVisible: (notebookModel: NotebookModel) => !Boolean(notebookModel?.readOnly), + execute: (notebookModel: NotebookModel) => { + execute(notebookModel); + } + }; + } + registerMenus(menus: MenuModelRegistry): void { // independent submenu for plugins to add commands menus.registerIndependentSubmenu(NotebookMenus.NOTEBOOK_MAIN_TOOLBAR, 'Notebook Main Toolbar'); diff --git a/packages/notebook/src/browser/contributions/notebook-cell-actions-contribution.ts b/packages/notebook/src/browser/contributions/notebook-cell-actions-contribution.ts index 39071c329623d..b3e1eade2be7d 100644 --- a/packages/notebook/src/browser/contributions/notebook-cell-actions-contribution.ts +++ b/packages/notebook/src/browser/contributions/notebook-cell-actions-contribution.ts @@ -14,7 +14,7 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { Command, CommandContribution, CommandRegistry, CompoundMenuNodeRole, MenuContribution, MenuModelRegistry, nls } from '@theia/core'; +import { Command, CommandContribution, CommandHandler, CommandRegistry, CompoundMenuNodeRole, MenuContribution, MenuModelRegistry, nls } from '@theia/core'; import { codicon } from '@theia/core/lib/browser'; import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; import { NotebookModel } from '../view-model/notebook-model'; @@ -172,30 +172,39 @@ export class NotebookCellActionContribution implements MenuContribution, Command } registerCommands(commands: CommandRegistry): void { - commands.registerCommand(NotebookCellCommands.EDIT_COMMAND, { execute: (_, cell: NotebookCellModel) => cell.requestEdit() }); + commands.registerCommand(NotebookCellCommands.EDIT_COMMAND, this.editableCellCommandHandler((_, cell) => cell.requestEdit())); commands.registerCommand(NotebookCellCommands.STOP_EDIT_COMMAND, { execute: (_, cell: NotebookCellModel) => cell.requestStopEdit() }); - commands.registerCommand(NotebookCellCommands.DELETE_COMMAND, { - execute: (notebookModel: NotebookModel, cell: NotebookCellModel) => notebookModel.applyEdits([{ + commands.registerCommand(NotebookCellCommands.DELETE_COMMAND, + this.editableCellCommandHandler((notebookModel, cell) => notebookModel.applyEdits([{ editType: CellEditType.Replace, index: notebookModel.cells.indexOf(cell), count: 1, cells: [] - }], true) - }); + }], true))); commands.registerCommand(NotebookCellCommands.SPLIT_CELL_COMMAND); - commands.registerCommand(NotebookCellCommands.EXECUTE_SINGLE_CELL_COMMAND, { - execute: (notebookModel: NotebookModel, cell: NotebookCellModel) => this.notebookExecutionService.executeNotebookCells(notebookModel, [cell]) - }); + commands.registerCommand(NotebookCellCommands.EXECUTE_SINGLE_CELL_COMMAND, this.editableCellCommandHandler( + (notebookModel, cell) => this.notebookExecutionService.executeNotebookCells(notebookModel, [cell]) + )); commands.registerCommand(NotebookCellCommands.STOP_CELL_EXECUTION_COMMAND, { execute: (notebookModel: NotebookModel, cell: NotebookCellModel) => this.notebookExecutionService.cancelNotebookCells(notebookModel, [cell]) }); - commands.registerCommand(NotebookCellCommands.CLEAR_OUTPUTS_COMMAND, { - execute: (_, cell: NotebookCellModel) => cell.spliceNotebookCellOutputs({ start: 0, deleteCount: cell.outputs.length, newOutputs: [] }) - }); - commands.registerCommand(NotebookCellCommands.CHANGE_OUTPUT_PRESENTATION_COMMAND, { - execute: (_, __, output: NotebookCellOutputModel) => output.requestOutputPresentationUpdate() - }); + commands.registerCommand(NotebookCellCommands.CLEAR_OUTPUTS_COMMAND, this.editableCellCommandHandler( + (_, cell) => cell.spliceNotebookCellOutputs({ start: 0, deleteCount: cell.outputs.length, newOutputs: [] }) + )); + commands.registerCommand(NotebookCellCommands.CHANGE_OUTPUT_PRESENTATION_COMMAND, this.editableCellCommandHandler( + (_, __, output) => output?.requestOutputPresentationUpdate() + )); + } + + protected editableCellCommandHandler(execute: (notebookModel: NotebookModel, cell: NotebookCellModel, output?: NotebookCellOutputModel) => void): CommandHandler { + return { + isEnabled: (notebookModel: NotebookModel) => !Boolean(notebookModel?.readOnly), + isVisible: (notebookModel: NotebookModel) => !Boolean(notebookModel?.readOnly), + execute: (notebookModel: NotebookModel, cell: NotebookCellModel, output?: NotebookCellOutputModel) => { + execute(notebookModel, cell, output); + } + }; } } diff --git a/packages/notebook/src/browser/notebook-cell-resource-resolver.ts b/packages/notebook/src/browser/notebook-cell-resource-resolver.ts index 9e2db9b92b1c4..1d6af928cc9b8 100644 --- a/packages/notebook/src/browser/notebook-cell-resource-resolver.ts +++ b/packages/notebook/src/browser/notebook-cell-resource-resolver.ts @@ -14,20 +14,37 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { Emitter, Resource, ResourceReadOptions, ResourceResolver, URI } from '@theia/core'; +import { Event, Emitter, Resource, ResourceReadOptions, ResourceResolver, URI } from '@theia/core'; import { inject, injectable } from '@theia/core/shared/inversify'; +import { MarkdownString } from '@theia/core/lib/common/markdown-rendering'; import { CellUri } from '../common'; import { NotebookService } from './service/notebook-service'; import { NotebookCellModel } from './view-model/notebook-cell-model'; +import { NotebookModel } from './view-model/notebook-model'; export class NotebookCellResource implements Resource { - protected readonly didChangeContentsEmitter = new Emitter(); - readonly onDidChangeContents = this.didChangeContentsEmitter.event; + protected readonly onDidChangeContentsEmitter = new Emitter(); + get onDidChangeContents(): Event { + return this.onDidChangeContentsEmitter.event; + } + + get onDidChangeReadOnly(): Event | undefined { + return this.notebook.onDidChangeReadOnly; + } + + get readOnly(): boolean | MarkdownString | undefined { + return this.notebook.readOnly; + } + + protected cell: NotebookCellModel; + protected notebook: NotebookModel; - private cell: NotebookCellModel; + uri: URI; - constructor(public uri: URI, cell: NotebookCellModel) { + constructor(uri: URI, notebook: NotebookModel, cell: NotebookCellModel) { + this.uri = uri; + this.notebook = notebook; this.cell = cell; } @@ -36,7 +53,7 @@ export class NotebookCellResource implements Resource { } dispose(): void { - this.didChangeContentsEmitter.dispose(); + this.onDidChangeContentsEmitter.dispose(); } } @@ -69,7 +86,7 @@ export class NotebookCellResourceResolver implements ResourceResolver { throw new Error(`No cell found with handle '${parsedUri.handle}' in '${parsedUri.notebook}'`); } - return new NotebookCellResource(uri, notebookCellModel); + return new NotebookCellResource(uri, notebookModel, notebookCellModel); } } diff --git a/packages/notebook/src/browser/notebook-editor-widget.tsx b/packages/notebook/src/browser/notebook-editor-widget.tsx index ca70be1b17a5a..be2a2577a40a0 100644 --- a/packages/notebook/src/browser/notebook-editor-widget.tsx +++ b/packages/notebook/src/browser/notebook-editor-widget.tsx @@ -16,7 +16,7 @@ import * as React from '@theia/core/shared/react'; import { CommandRegistry, MenuModelRegistry, URI } from '@theia/core'; -import { ReactWidget, Navigatable, SaveableSource, Message, DelegatingSaveable } from '@theia/core/lib/browser'; +import { ReactWidget, Navigatable, SaveableSource, Message, DelegatingSaveable, lock, unlock } from '@theia/core/lib/browser'; import { ReactNode } from '@theia/core/shared/react'; import { CellKind } from '../common'; import { CellRenderer as CellRenderer, NotebookCellListView } from './view/notebook-cell-list-view'; @@ -29,6 +29,7 @@ import { Emitter } from '@theia/core/shared/vscode-languageserver-protocol'; import { NotebookEditorWidgetService } from './service/notebook-editor-widget-service'; import { NotebookMainToolbarRenderer } from './view/notebook-main-toolbar'; import { Deferred } from '@theia/core/lib/common/promise-util'; +import { MarkdownString } from '@theia/core/lib/common/markdown-rendering'; export const NotebookEditorWidgetContainerFactory = Symbol('NotebookEditorWidgetContainerFactory'); @@ -81,6 +82,9 @@ export class NotebookEditorWidget extends ReactWidget implements Navigatable, Sa protected readonly onDidChangeModelEmitter = new Emitter(); readonly onDidChangeModel = this.onDidChangeModelEmitter.event; + protected readonly onDidChangeReadOnlyEmitter = new Emitter(); + readonly onDidChangeReadOnly = this.onDidChangeReadOnlyEmitter.event; + protected readonly renderers = new Map(); protected _model?: NotebookModel; protected _ready: Deferred = new Deferred(); @@ -106,6 +110,7 @@ export class NotebookEditorWidget extends ReactWidget implements Navigatable, Sa this.update(); this.toDispose.push(this.onDidChangeModelEmitter); + this.toDispose.push(this.onDidChangeReadOnlyEmitter); this.renderers.set(CellKind.Markup, this.markdownCellRenderer); this.renderers.set(CellKind.Code, this.codeCellRenderer); @@ -116,6 +121,18 @@ export class NotebookEditorWidget extends ReactWidget implements Navigatable, Sa this._model = await this.props.notebookData; this.saveable.delegate = this._model; this.toDispose.push(this._model); + this.toDispose.push(this._model.onDidChangeReadOnly(readOnly => { + if (readOnly) { + lock(this.title); + } else { + unlock(this.title); + } + this.onDidChangeReadOnlyEmitter.fire(readOnly); + this.update(); + })); + if (this._model.readOnly) { + lock(this.title); + } // Ensure that the model is loaded before adding the editor this.notebookEditorService.addNotebookEditor(this); this.update(); diff --git a/packages/notebook/src/browser/service/notebook-model-resolver-service.ts b/packages/notebook/src/browser/service/notebook-model-resolver-service.ts index 03f42c4997701..ff7e8d368ecd9 100644 --- a/packages/notebook/src/browser/service/notebook-model-resolver-service.ts +++ b/packages/notebook/src/browser/service/notebook-model-resolver-service.ts @@ -14,7 +14,7 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { Emitter, URI } from '@theia/core'; +import { Emitter, Resource, ResourceProvider, URI } from '@theia/core'; import { inject, injectable } from '@theia/core/shared/inversify'; import { UriComponents } from '@theia/core/lib/common/uri'; import { FileService } from '@theia/filesystem/lib/browser/file-service'; @@ -34,6 +34,9 @@ export class NotebookModelResolverService { @inject(FileService) protected fileService: FileService; + @inject(ResourceProvider) + protected resourceProvider: ResourceProvider; + @inject(NotebookService) protected notebookService: NotebookService; @@ -60,9 +63,9 @@ export class NotebookModelResolverService { throw new Error(`Missing viewType for '${resource}'`); } - const notebookData = await this.resolveExistingNotebookData(resource, viewType!); - - const notebookModel = await this.notebookService.createNotebookModel(notebookData, viewType, resource); + const actualResource = await this.resourceProvider(resource); + const notebookData = await this.resolveExistingNotebookData(actualResource, viewType!); + const notebookModel = await this.notebookService.createNotebookModel(notebookData, viewType, actualResource); notebookModel.onDirtyChanged(() => this.onDidChangeDirtyEmitter.fire(notebookModel)); notebookModel.onDidSaveNotebook(() => this.onDidSaveNotebookEmitter.fire(notebookModel.uri.toComponents())); @@ -103,8 +106,8 @@ export class NotebookModelResolverService { return this.resolve(resource, viewType); } - protected async resolveExistingNotebookData(resource: URI, viewType: string): Promise { - if (resource.scheme === 'untitled') { + protected async resolveExistingNotebookData(resource: Resource, viewType: string): Promise { + if (resource.uri.scheme === 'untitled') { return { cells: [ @@ -118,10 +121,11 @@ export class NotebookModelResolverService { metadata: {} }; } else { - const file = await this.fileService.readFile(resource); - - const dataProvider = await this.notebookService.getNotebookDataProvider(viewType); - const notebook = await dataProvider.serializer.toNotebook(file.value); + const [dataProvider, contents] = await Promise.all([ + this.notebookService.getNotebookDataProvider(viewType), + this.fileService.readFile(resource.uri) + ]); + const notebook = await dataProvider.serializer.toNotebook(contents.value); return notebook; } diff --git a/packages/notebook/src/browser/service/notebook-service.ts b/packages/notebook/src/browser/service/notebook-service.ts index da6e1920e4ba2..583dac957a0f5 100644 --- a/packages/notebook/src/browser/service/notebook-service.ts +++ b/packages/notebook/src/browser/service/notebook-service.ts @@ -14,7 +14,7 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { Disposable, DisposableCollection, Emitter, URI } from '@theia/core'; +import { Disposable, DisposableCollection, Emitter, Resource, URI } from '@theia/core'; import { inject, injectable } from '@theia/core/shared/inversify'; import { BinaryBuffer } from '@theia/core/lib/common/buffer'; import { NotebookData, TransientOptions } from '../../common'; @@ -101,14 +101,14 @@ export class NotebookService implements Disposable { }); } - async createNotebookModel(data: NotebookData, viewType: string, uri: URI): Promise { + async createNotebookModel(data: NotebookData, viewType: string, resource: Resource): Promise { const serializer = this.notebookProviders.get(viewType)?.serializer; if (!serializer) { throw new Error('no notebook serializer for ' + viewType); } - const model = this.notebookModelFactory({ data, uri, viewType, serializer }); - this.notebookModels.set(uri.toString(), model); + const model = this.notebookModelFactory({ data, resource, viewType, serializer }); + this.notebookModels.set(resource.uri.toString(), model); // Resolve cell text models right after creating the notebook model // This ensures that all text models are available in the plugin host await Promise.all(model.cells.map(e => e.resolveTextModel())); diff --git a/packages/notebook/src/browser/style/index.css b/packages/notebook/src/browser/style/index.css index 2c9b1844a0641..8e7334408a001 100644 --- a/packages/notebook/src/browser/style/index.css +++ b/packages/notebook/src/browser/style/index.css @@ -26,11 +26,14 @@ } .theia-notebook-cell { - cursor: grab; display: flex; margin: 10px 0px; } +.theia-notebook-cell.draggable { + cursor: grab; +} + .theia-notebook-cell:hover .theia-notebook-cell-marker { visibility: visible; } 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 1389d59c0dd7b..8a02da4c5c7d7 100644 --- a/packages/notebook/src/browser/view-model/notebook-cell-model.ts +++ b/packages/notebook/src/browser/view-model/notebook-cell-model.ts @@ -150,7 +150,7 @@ export class NotebookCellModel implements NotebookCell, Disposable { } - textModel: MonacoEditorModel; + protected textModel?: MonacoEditorModel; protected htmlContext: HTMLLIElement; @@ -220,12 +220,14 @@ export class NotebookCellModel implements NotebookCell, Disposable { this.onDidChangeInternalMetadataEmitter.dispose(); this.onDidChangeLanguageEmitter.dispose(); this.notebookCellContextManager.dispose(); - this.textModel.dispose(); + this.textModel?.dispose(); this.toDispose.dispose(); } requestEdit(): void { - this.onDidRequestCellEditChangeEmitter.fire(true); + if (!this.textModel || !this.textModel.readOnly) { + this.onDidRequestCellEditChangeEmitter.fire(true); + } } requestStopEdit(): void { diff --git a/packages/notebook/src/browser/view-model/notebook-model.ts b/packages/notebook/src/browser/view-model/notebook-model.ts index 9ec960c104787..a1236f63d18c7 100644 --- a/packages/notebook/src/browser/view-model/notebook-model.ts +++ b/packages/notebook/src/browser/view-model/notebook-model.ts @@ -14,7 +14,7 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { Disposable, Emitter, URI } from '@theia/core'; +import { Disposable, Emitter, Event, Resource, URI } from '@theia/core'; import { Saveable, SaveOptions } from '@theia/core/lib/browser'; import { CellData, CellEditType, CellUri, NotebookCellInternalMetadata, @@ -28,6 +28,7 @@ import { NotebookCellModel, NotebookCellModelFactory } from './notebook-cell-mod import { MonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service'; import { inject, injectable, interfaces, postConstruct } from '@theia/core/shared/inversify'; import { UndoRedoService } from '@theia/editor/lib/browser/undo-redo-service'; +import { MarkdownString } from '@theia/core/lib/common/markdown-rendering'; export const NotebookModelFactory = Symbol('NotebookModelFactory'); @@ -42,10 +43,10 @@ export function createNotebookModelContainer(parent: interfaces.Container, props const NotebookModelProps = Symbol('NotebookModelProps'); export interface NotebookModelProps { - data: NotebookData, - uri: URI, - viewType: string, - serializer: NotebookSerializer, + data: NotebookData; + resource: Resource; + viewType: string; + serializer: NotebookSerializer; } @injectable() @@ -63,6 +64,10 @@ export class NotebookModel implements Saveable, Disposable { protected readonly onDidChangeContentEmitter = new Emitter(); readonly onDidChangeContent = this.onDidChangeContentEmitter.event; + get onDidChangeReadOnly(): Event { + return this.props.resource.onDidChangeReadOnly ?? Event.None; + } + @inject(FileService) protected readonly fileService: FileService; @@ -81,7 +86,7 @@ export class NotebookModel implements Saveable, Disposable { protected nextHandle: number = 0; - protected _dirty: boolean = false; + protected _dirty = false; set dirty(dirty: boolean) { this._dirty = dirty; @@ -92,13 +97,17 @@ export class NotebookModel implements Saveable, Disposable { return this._dirty; } + get readOnly(): boolean | MarkdownString { + return this.props.resource.readOnly ?? false; + } + selectedCell?: NotebookCellModel; protected dirtyCells: NotebookCellModel[] = []; cells: NotebookCellModel[]; get uri(): URI { - return this.props.uri; + return this.props.resource.uri; } get viewType(): string { @@ -112,7 +121,7 @@ export class NotebookModel implements Saveable, Disposable { this.dirty = false; this.cells = this.props.data.cells.map((cell, index) => this.cellModelFactory({ - uri: CellUri.generate(this.props.uri, index), + uri: CellUri.generate(this.props.resource.uri, index), handle: index, source: cell.source, language: cell.language, @@ -294,7 +303,7 @@ export class NotebookModel implements Saveable, Disposable { } } - private replaceCells(start: number, deleteCount: number, newCells: CellData[], computeUndoRedo: boolean): void { + protected async replaceCells(start: number, deleteCount: number, newCells: CellData[], computeUndoRedo: boolean): Promise { const cells = newCells.map(cell => { const handle = this.nextHandle++; return this.cellModelFactory({ @@ -325,11 +334,15 @@ export class NotebookModel implements Saveable, Disposable { async () => this.replaceCells(start, deleteCount, newCells, false)); } + // Ensure that all text model have been created + // Otherwise we run into a race condition once we fire `onDidChangeContent` + await Promise.all(cells.map(cell => cell.resolveTextModel())); + this.onDidAddOrRemoveCellEmitter.fire({ rawEvent: { kind: NotebookCellsChangeType.ModelChange, changes }, newCellIds: cells.map(cell => cell.handle) }); this.onDidChangeContentEmitter.fire([{ kind: NotebookCellsChangeType.ModelChange, changes }]); } - private changeCellInternalMetadataPartial(cell: NotebookCellModel, internalMetadata: NullablePartialNotebookCellInternalMetadata): void { + protected changeCellInternalMetadataPartial(cell: NotebookCellModel, internalMetadata: NullablePartialNotebookCellInternalMetadata): void { const newInternalMetadata: NotebookCellInternalMetadata = { ...cell.internalMetadata }; @@ -345,7 +358,7 @@ export class NotebookModel implements Saveable, Disposable { ]); } - private updateNotebookMetadata(metadata: NotebookDocumentMetadata, computeUndoRedo: boolean): void { + protected updateNotebookMetadata(metadata: NotebookDocumentMetadata, computeUndoRedo: boolean): void { const oldMetadata = this.metadata; if (computeUndoRedo) { this.undoRedoService.pushElement(this.uri, @@ -358,7 +371,7 @@ export class NotebookModel implements Saveable, Disposable { this.onDidChangeContentEmitter.fire([{ kind: NotebookCellsChangeType.ChangeDocumentMetadata, metadata: this.metadata }]); } - private changeCellLanguage(cell: NotebookCellModel, languageId: string, computeUndoRedo: boolean): void { + protected changeCellLanguage(cell: NotebookCellModel, languageId: string, computeUndoRedo: boolean): void { if (cell.language === languageId) { return; } @@ -368,7 +381,7 @@ export class NotebookModel implements Saveable, Disposable { this.onDidChangeContentEmitter.fire([{ kind: NotebookCellsChangeType.ChangeCellLanguage, index: this.cells.indexOf(cell), language: languageId }]); } - private moveCellToIndex(fromIndex: number, length: number, toIndex: number, computeUndoRedo: boolean): boolean { + 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); }, @@ -383,7 +396,7 @@ export class NotebookModel implements Saveable, Disposable { return true; } - private getCellIndexByHandle(handle: number): number { + protected getCellIndexByHandle(handle: number): number { return this.cells.findIndex(c => c.handle === handle); } } 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 f1ea9aab4945a..6b17495120953 100644 --- a/packages/notebook/src/browser/view/notebook-cell-list-view.tsx +++ b/packages/notebook/src/browser/view/notebook-cell-list-view.tsx @@ -64,11 +64,13 @@ export class NotebookCellListView extends React.Component - this.onAddNewCell(kind, index)} + this.isEnabled()} + onAddNewCell={(kind: CellKind) => this.onAddNewCell(kind, index)} onDrop={e => this.onDrop(e, index)} onDragOver={e => this.onDragOver(e, cell, 'top')} /> {this.shouldRenderDragOverIndicator(cell, 'top') && } -
  • { this.setState({ selectedCell: cell }); this.props.notebookModel.setSelectedCell(cell); @@ -89,7 +91,9 @@ export class NotebookCellListView extends React.Component ) } - this.onAddNewCell(kind, this.props.notebookModel.cells.length)} + this.isEnabled()} + onAddNewCell={(kind: CellKind) => this.onAddNewCell(kind, this.props.notebookModel.cells.length)} onDrop={e => this.onDrop(e, this.props.notebookModel.cells.length - 1)} onDragOver={e => this.onDragOver(e, this.props.notebookModel.cells[this.props.notebookModel.cells.length - 1], 'bottom')} /> ; @@ -105,18 +109,33 @@ export class NotebookCellListView extends React.Component, index: number): void { event.stopPropagation(); + if (!this.isEnabled()) { + event.preventDefault(); + return; + } event.dataTransfer.setData('text/theia-notebook-cell-index', index.toString()); event.dataTransfer.setData('text/plain', this.props.notebookModel.cells[index].source); } protected onDragOver(event: React.DragEvent, cell: NotebookCellModel, position?: 'top' | 'bottom'): void { + if (!this.isEnabled()) { + return; + } event.preventDefault(); event.stopPropagation(); // show indicator this.setState({ ...this.state, dragOverIndicator: { cell, position: position ?? event.nativeEvent.offsetY < event.currentTarget.clientHeight / 2 ? 'top' : 'bottom' } }); } + protected isEnabled(): boolean { + return !Boolean(this.props.notebookModel.readOnly); + } + protected onDrop(event: React.DragEvent, dropElementIndex: number): void { + if (!this.isEnabled()) { + this.setState({ dragOverIndicator: undefined }); + return; + } const index = parseInt(event.dataTransfer.getData('text/theia-notebook-cell-index')); const isTargetBelow = index < dropElementIndex; let newIdx = this.state.dragOverIndicator?.position === 'top' ? dropElementIndex : dropElementIndex + 1; @@ -133,15 +152,18 @@ export class NotebookCellListView extends React.Component boolean; onAddNewCell: (type: CellKind) => void; onDrop: (event: React.DragEvent) => void; onDragOver: (event: React.DragEvent) => void; } -export function NotebookCellDivider({ onAddNewCell, onDrop, onDragOver }: NotebookCellDividerProps): React.JSX.Element { +export function NotebookCellDivider({ isVisible, onAddNewCell, onDrop, onDragOver }: NotebookCellDividerProps): React.JSX.Element { const [hover, setHover] = React.useState(false); return
  • setHover(true)} onMouseLeave={() => setHover(false)} onDrop={onDrop} onDragOver={onDragOver}> - {hover &&
    + {hover && isVisible() &&