diff --git a/extensions/ipynb/package.json b/extensions/ipynb/package.json index d923904ef9d..b97bd42e6c8 100644 --- a/extensions/ipynb/package.json +++ b/extensions/ipynb/package.json @@ -15,7 +15,8 @@ ], "activationEvents": [ "onNotebook:jupyter-notebook", - "onNotebookSerializer:interactive" + "onNotebookSerializer:interactive", + "onNotebookSerializer:repl" ], "extensionKind": [ "workspace", diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index fa9bd76ed8a..1e7360f3064 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -171,6 +171,7 @@ export class MenuId { static readonly InteractiveCellDelete = new MenuId('InteractiveCellDelete'); static readonly InteractiveCellExecute = new MenuId('InteractiveCellExecute'); static readonly InteractiveInputExecute = new MenuId('InteractiveInputExecute'); + static readonly ReplInputExecute = new MenuId('ReplInputExecute'); static readonly IssueReporter = new MenuId('IssueReporter'); static readonly NotebookToolbar = new MenuId('NotebookToolbar'); static readonly NotebookStickyScrollContext = new MenuId('NotebookStickyScrollContext'); diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index 3a7e105c843..5b707fc865e 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -36,7 +36,7 @@ import { IExtHostSearch } from 'vs/workbench/api/common/extHostSearch'; import { CellSearchModel } from 'vs/workbench/contrib/search/common/cellSearchModel'; import { INotebookCellMatchNoModel, INotebookFileMatchNoModel, IRawClosedNotebookFileMatch, genericCellMatchesToTextSearchMatches } from 'vs/workbench/contrib/search/common/searchNotebookHelpers'; import { NotebookPriorityInfo } from 'vs/workbench/contrib/search/common/search'; -import { globMatchesResource } from 'vs/workbench/services/editor/common/editorResolverService'; +import { globMatchesResource, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; import { ILogService } from 'vs/platform/log/common/log'; export class ExtHostNotebookController implements ExtHostNotebookShape { @@ -163,7 +163,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { providerDisplayName: extension.displayName || extension.name, displayName: registration.displayName, filenamePattern: viewOptionsFilenamePattern, - exclusive: registration.exclusive || false + priority: registration.exclusive ? RegisteredEditorPriority.exclusive : undefined }; } diff --git a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts index 24e4478a278..03b88f1f7af 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts @@ -51,9 +51,12 @@ import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/note import * as icons from 'vs/workbench/contrib/notebook/browser/notebookIcons'; import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService'; import { CellEditType, CellKind, CellUri, INTERACTIVE_WINDOW_EDITOR_ID, NotebookSetting, NotebookWorkingCopyTypeIdentifier } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { InteractiveWindowOpen } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { InteractiveWindowOpen, NOTEBOOK_CELL_LIST_FOCUSED, REPL_NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { executeReplInput } from 'vs/workbench/contrib/replNotebook/browser/repl.contribution'; +import { ReplEditor } from 'vs/workbench/contrib/replNotebook/browser/replEditor'; +import { ReplEditorInput } from 'vs/workbench/contrib/replNotebook/browser/replEditorInput'; import { columnToEditorGroup } from 'vs/workbench/services/editor/common/editorGroupColumn'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorResolverService, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; @@ -95,7 +98,7 @@ export class InteractiveDocumentContribution extends Disposable implements IWork providerDisplayName: 'Interactive Notebook', displayName: 'Interactive', filenamePattern: ['*.interactive'], - exclusive: true + priority: RegisteredEditorPriority.exclusive })); } @@ -411,10 +414,10 @@ registerAction2(class extends Action2 { logService.debug('Open new interactive window:', notebookUri.toString(), inputUri.toString()); if (id) { - const allKernels = kernelService.getMatchingKernel({ uri: notebookUri, viewType: 'interactive' }).all; + const allKernels = kernelService.getMatchingKernel({ uri: notebookUri, notebookType: 'interactive' }).all; const preferredKernel = allKernels.find(kernel => kernel.id === id); if (preferredKernel) { - kernelService.preselectKernelForNotebook(preferredKernel, { uri: notebookUri, viewType: 'interactive' }); + kernelService.preselectKernelForNotebook(preferredKernel, { uri: notebookUri, notebookType: 'interactive' }); } } @@ -456,10 +459,20 @@ registerAction2(class extends Action2 { primary: KeyMod.CtrlCmd | KeyCode.Enter }, weight: NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT + }, { + when: ContextKeyExpr.and( + REPL_NOTEBOOK_IS_ACTIVE_EDITOR, + NOTEBOOK_CELL_LIST_FOCUSED.toNegated() + ), + primary: KeyMod.CtrlCmd | KeyCode.Enter, + weight: NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT }], menu: [ { id: MenuId.InteractiveInputExecute + }, + { + id: MenuId.ReplInputExecute } ], icon: icons.executeIcon, @@ -483,21 +496,29 @@ registerAction2(class extends Action2 { const historyService = accessor.get(IInteractiveHistoryService); const notebookEditorService = accessor.get(INotebookEditorService); let editorControl: { notebookEditor: NotebookEditorWidget | undefined; codeEditor: CodeEditorWidget } | undefined; + let isReplEditor = false; if (context) { const resourceUri = URI.revive(context); - const editors = editorService.findEditors(resourceUri) - .filter(id => id.editor instanceof InteractiveEditorInput && id.editor.resource?.toString() === resourceUri.toString()); - if (editors.length) { - const editorInput = editors[0].editor as InteractiveEditorInput; - const currentGroup = editors[0].groupId; - const editor = await editorService.openEditor(editorInput, currentGroup); - editorControl = editor?.getControl() as { notebookEditor: NotebookEditorWidget | undefined; codeEditor: CodeEditorWidget } | undefined; + const editors = editorService.findEditors(resourceUri); + for (const found of editors) { + if (found.editor.typeId === ReplEditorInput.ID || found.editor.typeId === InteractiveEditorInput.ID) { + const editor = await editorService.openEditor(found.editor, found.groupId); + editorControl = editor?.getControl() as { notebookEditor: NotebookEditorWidget | undefined; codeEditor: CodeEditorWidget } | undefined; + isReplEditor = found.editor.typeId === ReplEditorInput.ID; + break; + } } } else { + const editor = editorService.activeEditorPane; + isReplEditor = editor instanceof ReplEditor; editorControl = editorService.activeEditorPane?.getControl() as { notebookEditor: NotebookEditorWidget | undefined; codeEditor: CodeEditorWidget } | undefined; } + if (editorControl && isReplEditor) { + executeReplInput(accessor, editorControl); + } + if (editorControl && editorControl.notebookEditor && editorControl.codeEditor) { const notebookDocument = editorControl.notebookEditor.textModel; const textModel = editorControl.codeEditor.getModel(); @@ -590,7 +611,7 @@ registerAction2(class extends Action2 { title: localize2('interactive.history.previous', 'Previous value in history'), category: interactiveWindowCategory, f1: false, - keybinding: { + keybinding: [{ when: ContextKeyExpr.and( ContextKeyExpr.equals('activeEditor', 'workbench.editor.interactive'), INTERACTIVE_INPUT_CURSOR_BOUNDARY.notEqualsTo('bottom'), @@ -599,7 +620,16 @@ registerAction2(class extends Action2 { ), primary: KeyCode.UpArrow, weight: KeybindingWeight.WorkbenchContrib - }, + }, { + when: ContextKeyExpr.and( + ContextKeyExpr.equals('activeEditor', 'workbench.editor.repl'), + INTERACTIVE_INPUT_CURSOR_BOUNDARY.notEqualsTo('bottom'), + INTERACTIVE_INPUT_CURSOR_BOUNDARY.notEqualsTo('none'), + SuggestContext.Visible.toNegated() + ), + primary: KeyCode.UpArrow, + weight: KeybindingWeight.WorkbenchContrib + }] }); } @@ -629,7 +659,7 @@ registerAction2(class extends Action2 { title: localize2('interactive.history.next', 'Next value in history'), category: interactiveWindowCategory, f1: false, - keybinding: { + keybinding: [{ when: ContextKeyExpr.and( ContextKeyExpr.equals('activeEditor', 'workbench.editor.interactive'), INTERACTIVE_INPUT_CURSOR_BOUNDARY.notEqualsTo('top'), @@ -638,7 +668,16 @@ registerAction2(class extends Action2 { ), primary: KeyCode.DownArrow, weight: KeybindingWeight.WorkbenchContrib - }, + }, { + when: ContextKeyExpr.and( + ContextKeyExpr.equals('activeEditor', 'workbench.editor.repl'), + INTERACTIVE_INPUT_CURSOR_BOUNDARY.notEqualsTo('top'), + INTERACTIVE_INPUT_CURSOR_BOUNDARY.notEqualsTo('none'), + SuggestContext.Visible.toNegated() + ), + primary: KeyCode.DownArrow, + weight: KeybindingWeight.WorkbenchContrib + }], }); } diff --git a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts index ba994f540b6..5f00a8ae37f 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts @@ -163,7 +163,7 @@ export class InteractiveEditor extends EditorPane implements IEditorPaneWithScro this._editorOptions = this._computeEditorOptions(); } })); - this._notebookOptions = new NotebookOptions(this.window, configurationService, notebookExecutionStateService, codeEditorService, true, { cellToolbarInteraction: 'hover', globalToolbar: true, stickyScrollEnabled: false, dragAndDropEnabled: false }); + this._notebookOptions = instantiationService.createInstance(NotebookOptions, this.window, true, { cellToolbarInteraction: 'hover', globalToolbar: true, stickyScrollEnabled: false, dragAndDropEnabled: false }); this._editorMemento = this.getEditorMemento(editorGroupService, textResourceConfigurationService, INTERACTIVE_EDITOR_VIEW_STATE_PREFERENCE_KEY); codeEditorService.registerDecorationType('interactive-decoration', DECORATION_KEY, {}); diff --git a/src/vs/workbench/contrib/notebook/browser/controller/apiActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/apiActions.ts index 1d5d4405f06..e69da8b479d 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/apiActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/apiActions.ts @@ -64,7 +64,7 @@ CommandsRegistry.registerCommand('_resolveNotebookKernels', async (accessor, arg }[]> => { const notebookKernelService = accessor.get(INotebookKernelService); const uri = URI.revive(args.uri as UriComponents); - const kernels = notebookKernelService.getMatchingKernel({ uri, viewType: args.viewType }); + const kernels = notebookKernelService.getMatchingKernel({ uri, notebookType: args.viewType }); return kernels.all.map(provider => ({ id: provider.id, diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts index 29dc02777d3..dd98ad9d3a4 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts @@ -41,13 +41,11 @@ import { BackLayerWebView, INotebookDelegateForWebview } from 'vs/workbench/cont import { NotebookDiffEditorEventDispatcher, NotebookDiffLayoutChangedEvent } from 'vs/workbench/contrib/notebook/browser/diff/eventDispatcher'; import { FontMeasurements } from 'vs/editor/browser/config/fontMeasurements'; import { NotebookOptions } from 'vs/workbench/contrib/notebook/browser/notebookOptions'; -import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { cellIndexesToRanges, cellRangesToIndexes } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { NotebookDiffOverviewRuler } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffOverviewRuler'; import { registerZIndex, ZIndex } from 'vs/platform/layout/browser/zIndexRegistry'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; const $ = DOM.$; @@ -151,11 +149,9 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD @IConfigurationService private readonly configurationService: IConfigurationService, @ITelemetryService telemetryService: ITelemetryService, @IStorageService storageService: IStorageService, - @INotebookExecutionStateService notebookExecutionStateService: INotebookExecutionStateService, - @ICodeEditorService codeEditorService: ICodeEditorService ) { super(NotebookTextDiffEditor.ID, group, telemetryService, themeService, storageService); - this._notebookOptions = new NotebookOptions(this.window, this.configurationService, notebookExecutionStateService, codeEditorService, false); + this._notebookOptions = instantiationService.createInstance(NotebookOptions, this.window, false, undefined); this._register(this._notebookOptions); this._revealFirst = true; } diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index c886a602b38..423c6165f73 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -187,8 +187,8 @@ class NotebookDiffEditorSerializer implements IEditorSerializer { } type SerializedNotebookEditorData = { resource: URI; preferredResource: URI; viewType: string; options?: NotebookEditorInputOptions }; class NotebookEditorSerializer implements IEditorSerializer { - canSerialize(): boolean { - return true; + canSerialize(input: EditorInput): boolean { + return input.typeId === NotebookEditorInput.ID; } serialize(input: EditorInput): string { assertType(input instanceof NotebookEditorInput); @@ -644,7 +644,7 @@ class SimpleNotebookWorkingCopyEditorHandler extends Disposable implements IWork private handlesSync(workingCopy: IWorkingCopyIdentifier): string /* viewType */ | undefined { const viewType = this._getViewType(workingCopy); - if (!viewType || viewType === 'interactive') { + if (!viewType || viewType === 'interactive' || extname(workingCopy.resource) === '.replNotebook') { return undefined; } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index f474d202cb1..dce4811e235 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -22,7 +22,7 @@ import { IEditorPane, IEditorPaneWithSelection } from 'vs/workbench/common/edito import { CellViewModelStateChangeEvent, NotebookCellStateChangedEvent, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { CellKind, ICellOutput, INotebookCellStatusBarItem, INotebookRendererInfo, INotebookSearchOptions, IOrderedMimeType, NotebookCellInternalMetadata, NotebookCellMetadata, NOTEBOOK_EDITOR_ID } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, ICellOutput, INotebookCellStatusBarItem, INotebookRendererInfo, INotebookSearchOptions, IOrderedMimeType, NotebookCellInternalMetadata, NotebookCellMetadata, NOTEBOOK_EDITOR_ID, REPL_EDITOR_ID } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { isCompositeNotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; import { INotebookKernel } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { NotebookOptions } from 'vs/workbench/contrib/notebook/browser/notebookOptions'; @@ -383,6 +383,7 @@ export interface INotebookEditorCreationOptions { }; readonly options?: NotebookOptions; readonly codeWindow?: CodeWindow; + readonly forRepl?: boolean; } export interface INotebookWebviewMessage { @@ -448,7 +449,7 @@ export interface INotebookViewCellsUpdateEvent { export interface INotebookViewModel { notebookDocument: NotebookTextModel; - viewCells: ICellViewModel[]; + readonly viewCells: ICellViewModel[]; layoutInfo: NotebookLayoutInfo | null; onDidChangeViewCells: Event; onDidChangeSelection: Event; @@ -878,7 +879,9 @@ export function getNotebookEditorFromEditorPane(editorPane?: IEditorPane): INote const input = editorPane.input; - if (input && isCompositeNotebookEditorInput(input)) { + const isInteractiveEditor = input && isCompositeNotebookEditorInput(input); + + if (isInteractiveEditor || editorPane.getId() === REPL_EDITOR_ID) { return (editorPane.getControl() as { notebookEditor: INotebookEditor | undefined } | undefined)?.notebookEditor; } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index addc97632f0..ea03755d75d 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -99,7 +99,6 @@ import { NotebookStickyScroll } from 'vs/workbench/contrib/notebook/browser/view import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { PixelRatio } from 'vs/base/browser/pixelRatio'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { PreventDefaultContextMenuItemsContextKeyName } from 'vs/workbench/contrib/webview/browser/webview.contribution'; import { NotebookAccessibilityProvider } from 'vs/workbench/contrib/notebook/browser/notebookAccessibilityProvider'; @@ -272,6 +271,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD readonly isEmbedded: boolean; private _readOnly: boolean; + private readonly _inRepl: boolean; public readonly scopedContextKeyService: IContextKeyService; private readonly instantiationService: IInstantiationService; @@ -302,7 +302,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD @IEditorProgressService private editorProgressService: IEditorProgressService, @INotebookLoggingService private readonly logService: INotebookLoggingService, @IKeybindingService private readonly keybindingService: IKeybindingService, - @ICodeEditorService codeEditorService: ICodeEditorService ) { super(); @@ -310,8 +309,14 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this.isEmbedded = creationOptions.isEmbedded ?? false; this._readOnly = creationOptions.isReadOnly ?? false; + this._inRepl = creationOptions.forRepl ?? false; - this._notebookOptions = creationOptions.options ?? new NotebookOptions(this.creationOptions?.codeWindow ?? mainWindow, this.configurationService, notebookExecutionStateService, codeEditorService, this._readOnly); + this._overlayContainer = document.createElement('div'); + this.scopedContextKeyService = this._register(contextKeyService.createScoped(this._overlayContainer)); + this.instantiationService = this._register(instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService]))); + + this._notebookOptions = creationOptions.options ?? + this.instantiationService.createInstance(NotebookOptions, this.creationOptions?.codeWindow ?? mainWindow, this._readOnly, undefined); this._register(this._notebookOptions); const eventDispatcher = this._register(new NotebookEventDispatcher()); this._viewContext = new ViewContext( @@ -322,9 +327,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._onDidChangeCellState.fire(e); })); - this._overlayContainer = document.createElement('div'); - this.scopedContextKeyService = this._register(contextKeyService.createScoped(this._overlayContainer)); - this.instantiationService = this._register(instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService]))); this._register(_notebookService.onDidChangeOutputRenderers(() => { this._updateOutputRenderers(); @@ -1435,7 +1437,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD private async _attachModel(textModel: NotebookTextModel, viewState: INotebookEditorViewState | undefined, perf?: NotebookPerfMarks) { this._ensureWebview(this.getId(), textModel.viewType, textModel.uri); - this.viewModel = this.instantiationService.createInstance(NotebookViewModel, textModel.viewType, textModel, this._viewContext, this.getLayoutInfo(), { isReadOnly: this._readOnly }); + this.viewModel = this.instantiationService.createInstance(NotebookViewModel, textModel.viewType, textModel, this._viewContext, this.getLayoutInfo(), { isReadOnly: this._readOnly, inRepl: this._inRepl }); this._viewContext.eventDispatcher.emit([new NotebookLayoutChangedEvent({ width: true, fontInfo: true }, this.getLayoutInfo())]); this.notebookOptions.updateOptions(this._readOnly); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookOptions.ts b/src/vs/workbench/contrib/notebook/browser/notebookOptions.ts index 46ce122a004..c83be8c872f 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookOptions.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookOptions.ts @@ -137,11 +137,11 @@ export class NotebookOptions extends Disposable { constructor( readonly targetWindow: CodeWindow, - private readonly configurationService: IConfigurationService, - private readonly notebookExecutionStateService: INotebookExecutionStateService, - private readonly codeEditorService: ICodeEditorService, private isReadonly: boolean, - private readonly overrides?: { cellToolbarInteraction: string; globalToolbar: boolean; stickyScrollEnabled: boolean; dragAndDropEnabled: boolean } + private readonly overrides: { cellToolbarInteraction: string; globalToolbar: boolean; stickyScrollEnabled: boolean; dragAndDropEnabled: boolean } | undefined, + @IConfigurationService private readonly configurationService: IConfigurationService, + @INotebookExecutionStateService private readonly notebookExecutionStateService: INotebookExecutionStateService, + @ICodeEditorService private readonly codeEditorService: ICodeEditorService, ) { super(); const showCellStatusBar = this.configurationService.getValue(NotebookSetting.showCellStatusBar); diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookKernelHistoryServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookKernelHistoryServiceImpl.ts index 3e3446349b8..0c3852191da 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookKernelHistoryServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookKernelHistoryServiceImpl.ts @@ -48,7 +48,7 @@ export class NotebookKernelHistoryService extends Disposable implements INoteboo // We will suggest the only kernel const suggested = allAvailableKernels.all.length === 1 ? allAvailableKernels.all[0] : undefined; this._notebookLoggingService.debug('History', `getMatchingKernels: ${allAvailableKernels.all.length} kernels available for ${notebook.uri.path}. Selected: ${allAvailableKernels.selected?.label}. Suggested: ${suggested?.label}`); - const mostRecentKernelIds = this._mostRecentKernelsMap[notebook.viewType] ? [...this._mostRecentKernelsMap[notebook.viewType].values()] : []; + const mostRecentKernelIds = this._mostRecentKernelsMap[notebook.notebookType] ? [...this._mostRecentKernelsMap[notebook.notebookType].values()] : []; const all = mostRecentKernelIds.map(kernelId => allKernels.find(kernel => kernel.id === kernelId)).filter(kernel => !!kernel) as INotebookKernel[]; this._notebookLoggingService.debug('History', `mru: ${mostRecentKernelIds.length} kernels in history, ${all.length} registered already.`); diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookKernelServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookKernelServiceImpl.ts index 572a833e173..05174803267 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookKernelServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookKernelServiceImpl.ts @@ -37,12 +37,12 @@ class KernelInfo { class NotebookTextModelLikeId { static str(k: INotebookTextModelLike): string { - return `${k.viewType}/${k.uri.toString()}`; + return `${k.notebookType}/${k.uri.toString()}`; } static obj(s: string): INotebookTextModelLike { const idx = s.indexOf('/'); return { - viewType: s.substring(0, idx), + notebookType: s.substring(0, idx), uri: URI.parse(s.substring(idx + 1)) }; } @@ -178,7 +178,7 @@ export class NotebookKernelService extends Disposable implements INotebookKernel private static _score(kernel: INotebookKernel, notebook: INotebookTextModelLike): number { if (kernel.viewType === '*') { return 5; - } else if (kernel.viewType === notebook.viewType) { + } else if (kernel.viewType === notebook.notebookType) { return 10; } else { return 0; @@ -343,7 +343,7 @@ export class NotebookKernelService extends Disposable implements INotebookKernel const stateChangeListener = sourceAction.onDidChangeState(() => { this._onDidChangeSourceActions.fire({ notebook: document.uri, - viewType: document.viewType, + viewType: document.notebookType, }); }); sourceActions.push([sourceAction, stateChangeListener]); @@ -351,7 +351,7 @@ export class NotebookKernelService extends Disposable implements INotebookKernel }); info.actions = sourceActions; this._kernelSources.set(id, info); - this._onDidChangeSourceActions.fire({ notebook: document.uri, viewType: document.viewType }); + this._onDidChangeSourceActions.fire({ notebook: document.uri, viewType: document.notebookType }); }; this._kernelSourceActionsUpdates.get(id)?.dispose(); @@ -382,7 +382,7 @@ export class NotebookKernelService extends Disposable implements INotebookKernel } getKernelDetectionTasks(notebook: INotebookTextModelLike): INotebookKernelDetectionTask[] { - return this._kernelDetectionTasks.get(notebook.viewType) ?? []; + return this._kernelDetectionTasks.get(notebook.notebookType) ?? []; } registerKernelSourceActionProvider(viewType: string, provider: IKernelSourceActionProvider): IDisposable { @@ -411,7 +411,7 @@ export class NotebookKernelService extends Disposable implements INotebookKernel * Get kernel source actions from providers */ getKernelSourceActions2(notebook: INotebookTextModelLike): Promise { - const viewType = notebook.viewType; + const viewType = notebook.notebookType; const providers = this._kernelSourceActionProviders.get(viewType) ?? []; const promises = providers.map(provider => provider.provideKernelSourceActions()); return Promise.all(promises).then(actions => { diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts index 99a74727cb4..86a0b01c560 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts @@ -132,7 +132,6 @@ export class NotebookProviderInfoStore extends Disposable { selectors: notebookContribution.selector || [], priority: this._convertPriority(notebookContribution.priority), providerDisplayName: extension.description.displayName ?? extension.description.identifier.value, - exclusive: false })); } } @@ -171,7 +170,7 @@ export class NotebookProviderInfoStore extends Disposable { id: notebookProviderInfo.id, label: notebookProviderInfo.displayName, detail: notebookProviderInfo.providerDisplayName, - priority: notebookProviderInfo.exclusive ? RegisteredEditorPriority.exclusive : notebookProviderInfo.priority, + priority: notebookProviderInfo.priority, }; const notebookEditorOptions = { canHandleDiff: () => !!this._configurationService.getValue(NotebookSetting.textDiffEditorPreview) && !this._accessibilityService.isScreenReaderOptimized(), @@ -639,8 +638,7 @@ export class NotebookService extends Disposable implements INotebookService { id: viewType, displayName: data.displayName, providerDisplayName: data.providerDisplayName, - exclusive: data.exclusive, - priority: RegisteredEditorPriority.default, + priority: data.priority || RegisteredEditorPriority.default, selectors: [] }); diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts index 8a0f8379011..7ea020f145c 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts @@ -99,6 +99,7 @@ let MODEL_ID = 0; export interface NotebookViewModelOptions { isReadOnly: boolean; + inRepl?: boolean; } export class NotebookViewModel extends Disposable implements EditorFoldingStateDelegate, INotebookViewModel { @@ -108,15 +109,12 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD private readonly _onDidChangeOptions = this._register(new Emitter()); get onDidChangeOptions(): Event { return this._onDidChangeOptions.event; } private _viewCells: CellViewModel[] = []; + private readonly replView: boolean; get viewCells(): ICellViewModel[] { return this._viewCells; } - set viewCells(_: ICellViewModel[]) { - throw new Error('NotebookViewModel.viewCells is readonly'); - } - get length(): number { return this._viewCells.length; } @@ -206,6 +204,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD MODEL_ID++; this.id = '$notebookViewModel' + MODEL_ID; this._instanceId = strings.singleLetterHash(MODEL_ID); + this.replView = !!this.options.inRepl; const compute = (changes: NotebookCellTextModelSplice[], synchronous: boolean) => { const diffs = changes.map(splice => { @@ -337,9 +336,12 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD this._onDidChangeSelection.fire(e); })); - this._viewCells = this._notebook.cells.map(cell => { - return createCellViewModel(this._instantiationService, this, cell, this._viewContext); - }); + + const viewCellCount = this.replView ? this._notebook.cells.length - 1 : this._notebook.cells.length; + for (let i = 0; i < viewCellCount; i++) { + this._viewCells.push(createCellViewModel(this._instantiationService, this, this._notebook.cells[i], this._viewContext)); + } + this._viewCells.forEach(cell => { this._handleToViewCellMapping.set(cell.handle, cell); diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index b3b6ff6e2f9..21870a0067f 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -215,6 +215,10 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel return this._alternativeVersionId; } + get notebookType() { + return this.viewType; + } + constructor( readonly viewType: string, readonly uri: URI, diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 33c85b27420..bb08370538b 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -33,11 +33,15 @@ import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IFileReadLimits } from 'vs/platform/files/common/files'; import { parse as parseUri, generate as generateUri } from 'vs/workbench/services/notebook/common/notebookDocumentService'; import { ICellExecutionError } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; +import { INotebookTextModelLike } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; export const NOTEBOOK_EDITOR_ID = 'workbench.editor.notebook'; export const NOTEBOOK_DIFF_EDITOR_ID = 'workbench.editor.notebookTextDiffEditor'; export const INTERACTIVE_WINDOW_EDITOR_ID = 'workbench.editor.interactive'; +export const REPL_EDITOR_ID = 'workbench.editor.repl'; +export const EXECUTE_REPL_COMMAND_ID = 'replNotebook.input.execute'; export enum CellKind { Markup = 1, @@ -252,7 +256,8 @@ export interface ICell { onDidChangeInternalMetadata: Event; } -export interface INotebookTextModel { +export interface INotebookTextModel extends INotebookTextModelLike { + readonly notebookType: string; readonly viewType: string; metadata: NotebookDocumentMetadata; readonly transientOptions: TransientOptions; @@ -551,7 +556,7 @@ export interface INotebookContributionData { providerDisplayName: string; displayName: string; filenamePattern: (string | glob.IRelativePattern | INotebookExclusiveDocumentFilter)[]; - exclusive: boolean; + priority?: RegisteredEditorPriority; } @@ -776,6 +781,11 @@ export interface INotebookLoadOptions { readonly limits?: IFileReadLimits; } +export type NotebookEditorModelCreationOptions = { + limits?: IFileReadLimits; + scratchpad?: boolean; +}; + export interface IResolvedNotebookEditorModel extends INotebookEditorModel { notebook: NotebookTextModel; } diff --git a/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts b/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts index 4aad255f787..c4961dd6dad 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { ContextKeyExpr, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { INTERACTIVE_WINDOW_EDITOR_ID, NOTEBOOK_EDITOR_ID } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INTERACTIVE_WINDOW_EDITOR_ID, NOTEBOOK_EDITOR_ID, REPL_EDITOR_ID } from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -16,11 +16,15 @@ export const InteractiveWindowOpen = new RawContextKey('interactiveWind // Is Notebook export const NOTEBOOK_IS_ACTIVE_EDITOR = ContextKeyExpr.equals('activeEditor', NOTEBOOK_EDITOR_ID); export const INTERACTIVE_WINDOW_IS_ACTIVE_EDITOR = ContextKeyExpr.equals('activeEditor', INTERACTIVE_WINDOW_EDITOR_ID); +export const REPL_NOTEBOOK_IS_ACTIVE_EDITOR = ContextKeyExpr.equals('activeEditor', REPL_EDITOR_ID); // Editor keys +// based on the focus of the notebook editor widget export const NOTEBOOK_EDITOR_FOCUSED = new RawContextKey('notebookEditorFocused', false); +// always true within the cell list html element export const NOTEBOOK_CELL_LIST_FOCUSED = new RawContextKey('notebookCellListFocused', false); export const NOTEBOOK_OUTPUT_FOCUSED = new RawContextKey('notebookOutputFocused', false); +// an input html element within the output webview has focus export const NOTEBOOK_OUTPUT_INPUT_FOCUSED = new RawContextKey('notebookOutputInputFocused', false); export const NOTEBOOK_EDITOR_EDITABLE = new RawContextKey('notebookEditable', true); export const NOTEBOOK_HAS_RUNNING_CELL = new RawContextKey('notebookHasRunningCell', false); diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts index 0ac6de71100..5edcb35f62c 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts @@ -53,7 +53,7 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { static readonly ID: string = 'workbench.input.notebook'; - private _editorModelReference: IReference | null = null; + protected editorModelReference: IReference | null = null; private _sideLoadedListener: IDisposable; private _defaultDirtyState: boolean = false; @@ -105,8 +105,8 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { override dispose() { this._sideLoadedListener.dispose(); - this._editorModelReference?.dispose(); - this._editorModelReference = null; + this.editorModelReference?.dispose(); + this.editorModelReference = null; super.dispose(); } @@ -125,8 +125,8 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { capabilities |= EditorInputCapabilities.Untitled; } - if (this._editorModelReference) { - if (this._editorModelReference.object.isReadonly()) { + if (this.editorModelReference) { + if (this.editorModelReference.object.isReadonly()) { capabilities |= EditorInputCapabilities.Readonly; } } else { @@ -143,7 +143,7 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { } override getDescription(verbosity = Verbosity.MEDIUM): string | undefined { - if (!this.hasCapability(EditorInputCapabilities.Untitled) || this._editorModelReference?.object.hasAssociatedFilePath()) { + if (!this.hasCapability(EditorInputCapabilities.Untitled) || this.editorModelReference?.object.hasAssociatedFilePath()) { return super.getDescription(verbosity); } @@ -151,21 +151,21 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { } override isReadonly(): boolean | IMarkdownString { - if (!this._editorModelReference) { + if (!this.editorModelReference) { return this.filesConfigurationService.isReadonly(this.resource); } - return this._editorModelReference.object.isReadonly(); + return this.editorModelReference.object.isReadonly(); } override isDirty() { - if (!this._editorModelReference) { + if (!this.editorModelReference) { return this._defaultDirtyState; } - return this._editorModelReference.object.isDirty(); + return this.editorModelReference.object.isDirty(); } override isSaving(): boolean { - const model = this._editorModelReference?.object; + const model = this.editorModelReference?.object; if (!model || !model.isDirty() || model.hasErrorState || this.hasCapability(EditorInputCapabilities.Untitled)) { return false; // require the model to be dirty, file-backed and not in an error state } @@ -175,12 +175,12 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { } override async save(group: GroupIdentifier, options?: ISaveOptions): Promise { - if (this._editorModelReference) { + if (this.editorModelReference) { if (this.hasCapability(EditorInputCapabilities.Untitled)) { return this.saveAs(group, options); } else { - await this._editorModelReference.object.save(options); + await this.editorModelReference.object.save(options); } return this; @@ -190,7 +190,7 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { } override async saveAs(group: GroupIdentifier, options?: ISaveOptions): Promise { - if (!this._editorModelReference) { + if (!this.editorModelReference) { return undefined; } @@ -200,9 +200,9 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { return undefined; } - const pathCandidate = this.hasCapability(EditorInputCapabilities.Untitled) ? await this._suggestName(provider, this.labelService.getUriBasenameLabel(this.resource)) : this._editorModelReference.object.resource; + const pathCandidate = this.hasCapability(EditorInputCapabilities.Untitled) ? await this._suggestName(provider, this.labelService.getUriBasenameLabel(this.resource)) : this.editorModelReference.object.resource; let target: URI | undefined; - if (this._editorModelReference.object.hasAssociatedFilePath()) { + if (this.editorModelReference.object.hasAssociatedFilePath()) { target = pathCandidate; } else { target = await this._fileDialogService.pickFileToSave(pathCandidate, options?.availableFileSystems); @@ -231,7 +231,7 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { throw new Error(`File name ${target} is not supported by ${provider.providerDisplayName}.\n\nPlease make sure the file name matches following patterns:\n${patterns}`); } - return await this._editorModelReference.object.saveAs(target); + return await this.editorModelReference.object.saveAs(target); } private async _suggestName(provider: NotebookProviderInfo, suggestedFilename: string) { @@ -260,7 +260,7 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { // called when users rename a notebook document override async rename(group: GroupIdentifier, target: URI): Promise { - if (this._editorModelReference) { + if (this.editorModelReference) { return { editor: { resource: target }, options: { override: this.viewType } }; } @@ -268,8 +268,8 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { } override async revert(_group: GroupIdentifier, options?: IRevertOptions): Promise { - if (this._editorModelReference && this._editorModelReference.object.isDirty()) { - await this._editorModelReference.object.revert(options); + if (this.editorModelReference && this.editorModelReference.object.isDirty()) { + await this.editorModelReference.object.revert(options); } } @@ -284,42 +284,43 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { // "other" loading anymore this._sideLoadedListener.dispose(); - if (!this._editorModelReference) { - const ref = await this._notebookModelResolverService.resolve(this.resource, this.viewType, this.ensureLimits(_options)); - if (this._editorModelReference) { + if (!this.editorModelReference) { + const scratchpad = this.capabilities & EditorInputCapabilities.Scratchpad ? true : false; + const ref = await this._notebookModelResolverService.resolve(this.resource, this.viewType, { limits: this.ensureLimits(_options), scratchpad }); + if (this.editorModelReference) { // Re-entrant, double resolve happened. Dispose the addition references and proceed // with the truth. ref.dispose(); - return (>this._editorModelReference).object; + return (>this.editorModelReference).object; } - this._editorModelReference = ref; + this.editorModelReference = ref; if (this.isDisposed()) { - this._editorModelReference.dispose(); - this._editorModelReference = null; + this.editorModelReference.dispose(); + this.editorModelReference = null; return null; } - this._register(this._editorModelReference.object.onDidChangeDirty(() => this._onDidChangeDirty.fire())); - this._register(this._editorModelReference.object.onDidChangeReadonly(() => this._onDidChangeCapabilities.fire())); - this._register(this._editorModelReference.object.onDidRevertUntitled(() => this.dispose())); - if (this._editorModelReference.object.isDirty()) { + this._register(this.editorModelReference.object.onDidChangeDirty(() => this._onDidChangeDirty.fire())); + this._register(this.editorModelReference.object.onDidChangeReadonly(() => this._onDidChangeCapabilities.fire())); + this._register(this.editorModelReference.object.onDidRevertUntitled(() => this.dispose())); + if (this.editorModelReference.object.isDirty()) { this._onDidChangeDirty.fire(); } } else { - this._editorModelReference.object.load({ limits: this.ensureLimits(_options) }); + this.editorModelReference.object.load({ limits: this.ensureLimits(_options) }); } if (this.options._backupId) { - const info = await this._notebookService.withNotebookDataProvider(this._editorModelReference.object.notebook.viewType); + const info = await this._notebookService.withNotebookDataProvider(this.editorModelReference.object.notebook.viewType); if (!(info instanceof SimpleNotebookProviderInfo)) { throw new Error('CANNOT open file notebook with this provider'); } const data = await info.serializer.dataToNotebook(VSBuffer.fromString(JSON.stringify({ __webview_backup: this.options._backupId }))); - this._editorModelReference.object.notebook.applyEdits([ + this.editorModelReference.object.notebook.applyEdits([ { editType: CellEditType.Replace, index: 0, - count: this._editorModelReference.object.notebook.length, + count: this.editorModelReference.object.notebook.length, cells: data.cells } ], true, undefined, () => undefined, undefined, false); @@ -331,7 +332,7 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { } } - return this._editorModelReference.object; + return this.editorModelReference.object; } override toUntyped(): IResourceEditorInput { diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts index 3eff1eb5060..dc3d26a0ca3 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts @@ -5,10 +5,9 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { URI } from 'vs/base/common/uri'; -import { IResolvedNotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IResolvedNotebookEditorModel, NotebookEditorModelCreationOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IReference } from 'vs/base/common/lifecycle'; import { Event, IWaitUntil } from 'vs/base/common/event'; -import { IFileReadLimits } from 'vs/platform/files/common/files'; export const INotebookEditorModelResolverService = createDecorator('INotebookModelResolverService'); @@ -50,6 +49,6 @@ export interface INotebookEditorModelResolverService { isDirty(resource: URI): boolean; - resolve(resource: URI, viewType?: string, limits?: IFileReadLimits): Promise>; - resolve(resource: IUntitledNotebookResource, viewType: string, limits?: IFileReadLimits): Promise>; + resolve(resource: URI, viewType?: string, creationOptions?: NotebookEditorModelCreationOptions): Promise>; + resolve(resource: IUntitledNotebookResource, viewType: string, creationOtions?: NotebookEditorModelCreationOptions): Promise>; } diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts index 318a2a96731..d43273d4fa6 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts @@ -5,7 +5,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { URI } from 'vs/base/common/uri'; -import { CellUri, IResolvedNotebookEditorModel, NotebookSetting, NotebookWorkingCopyTypeIdentifier } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellUri, IResolvedNotebookEditorModel, NotebookEditorModelCreationOptions, NotebookSetting, NotebookWorkingCopyTypeIdentifier } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookFileWorkingCopyModel, NotebookFileWorkingCopyModelFactory, SimpleNotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel'; import { combinedDisposable, DisposableStore, dispose, IDisposable, IReference, ReferenceCollection, toDisposable } from 'vs/base/common/lifecycle'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; @@ -61,7 +61,7 @@ class NotebookModelReferenceCollection extends ReferenceCollection { + protected async createReferencedObject(key: string, viewType: string, hasAssociatedFilePath: boolean, limits?: IFileReadLimits, isScratchpad?: boolean): Promise { // Untrack as being disposed this.modelsToDispose.delete(key); @@ -79,8 +79,9 @@ class NotebookModelReferenceCollection extends ReferenceCollection(NotebookSetting.InteractiveWindowPromptToSave) !== true; - const model = this._instantiationService.createInstance(SimpleNotebookEditorModel, uri, hasAssociatedFilePath, viewType, workingCopyManager, scratchPad); + + const isScratchpadView = isScratchpad || (viewType === 'interactive' && this._configurationService.getValue(NotebookSetting.InteractiveWindowPromptToSave) !== true); + const model = this._instantiationService.createInstance(SimpleNotebookEditorModel, uri, hasAssociatedFilePath, viewType, workingCopyManager, isScratchpadView); const result = await model.load({ limits }); @@ -176,9 +177,9 @@ export class NotebookModelResolverServiceImpl implements INotebookEditorModelRes return this._data.isDirty(resource); } - async resolve(resource: URI, viewType?: string, limits?: IFileReadLimits): Promise>; - async resolve(resource: IUntitledNotebookResource, viewType: string, limits?: IFileReadLimits): Promise>; - async resolve(arg0: URI | IUntitledNotebookResource, viewType?: string, limits?: IFileReadLimits): Promise> { + async resolve(resource: URI, viewType?: string, options?: NotebookEditorModelCreationOptions): Promise>; + async resolve(resource: IUntitledNotebookResource, viewType: string, options: NotebookEditorModelCreationOptions): Promise>; + async resolve(arg0: URI | IUntitledNotebookResource, viewType?: string, options?: NotebookEditorModelCreationOptions): Promise> { let resource: URI; let hasAssociatedFilePath = false; if (URI.isUri(arg0)) { @@ -219,8 +220,9 @@ export class NotebookModelResolverServiceImpl implements INotebookEditorModelRes } else { await this._extensionService.whenInstalledExtensionsRegistered(); const providers = this._notebookService.getContributedNotebookTypes(resource); - const exclusiveProvider = providers.find(provider => provider.exclusive); - viewType = exclusiveProvider?.id || providers[0]?.id; + viewType = providers.find(provider => provider.priority === 'exclusive')?.id ?? + providers.find(provider => provider.priority === 'default')?.id ?? + providers[0]?.id; } } @@ -239,7 +241,7 @@ export class NotebookModelResolverServiceImpl implements INotebookEditorModelRes } } - const reference = this._data.acquire(resource.toString(), viewType, hasAssociatedFilePath, limits); + const reference = this._data.acquire(resource.toString(), viewType, hasAssociatedFilePath, options?.limits, options?.scratchpad); try { const model = await reference.object; return { diff --git a/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts b/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts index be999203d75..2351e4b42b2 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts @@ -106,7 +106,7 @@ export interface IKernelSourceActionProvider { provideKernelSourceActions(): Promise; } -export interface INotebookTextModelLike { uri: URI; viewType: string } +export interface INotebookTextModelLike { uri: URI; notebookType: string } export const INotebookKernelService = createDecorator('INotebookKernelService'); diff --git a/src/vs/workbench/contrib/notebook/common/notebookProvider.ts b/src/vs/workbench/contrib/notebook/common/notebookProvider.ts index 16a9ee3a57b..e2911dcf9e1 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookProvider.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookProvider.ts @@ -19,7 +19,6 @@ export interface NotebookEditorDescriptor { readonly selectors: readonly { filenamePattern?: string; excludeFileNamePattern?: string }[]; readonly priority: RegisteredEditorPriority; readonly providerDisplayName: string; - readonly exclusive: boolean; } export class NotebookProviderInfo { @@ -29,7 +28,6 @@ export class NotebookProviderInfo { readonly displayName: string; readonly priority: RegisteredEditorPriority; readonly providerDisplayName: string; - readonly exclusive: boolean; private _selectors: NotebookSelector[]; get selectors() { @@ -50,7 +48,6 @@ export class NotebookProviderInfo { })) || []; this.priority = descriptor.priority; this.providerDisplayName = descriptor.providerDisplayName; - this.exclusive = descriptor.exclusive; this._options = { transientCellMetadata: {}, transientDocumentMetadata: {}, diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookKernelHistory.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookKernelHistory.test.ts index 237ec6bd599..a037768bbed 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookKernelHistory.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookKernelHistory.test.ts @@ -66,8 +66,8 @@ suite('NotebookKernelHistoryService', () => { const u1 = URI.parse('foo:///one'); - const k1 = new TestNotebookKernel({ label: 'z', viewType: 'foo' }); - const k2 = new TestNotebookKernel({ label: 'a', viewType: 'foo' }); + const k1 = new TestNotebookKernel({ label: 'z', notebookType: 'foo' }); + const k2 = new TestNotebookKernel({ label: 'a', notebookType: 'foo' }); disposables.add(kernelService.registerKernel(k1)); disposables.add(kernelService.registerKernel(k2)); @@ -102,14 +102,14 @@ suite('NotebookKernelHistoryService', () => { const kernelHistoryService = disposables.add(instantiationService.createInstance(NotebookKernelHistoryService)); - let info = kernelHistoryService.getKernels({ uri: u1, viewType: 'foo' }); + let info = kernelHistoryService.getKernels({ uri: u1, notebookType: 'foo' }); assert.equal(info.all.length, 0); assert.ok(!info.selected); // update priorities for u1 notebook kernelService.updateKernelNotebookAffinity(k2, u1, 2); - info = kernelHistoryService.getKernels({ uri: u1, viewType: 'foo' }); + info = kernelHistoryService.getKernels({ uri: u1, notebookType: 'foo' }); assert.equal(info.all.length, 0); // MRU only auto selects kernel if there is only one assert.deepStrictEqual(info.selected, undefined); @@ -119,9 +119,9 @@ suite('NotebookKernelHistoryService', () => { const u1 = URI.parse('foo:///one'); - const k1 = new TestNotebookKernel({ label: 'z', viewType: 'foo' }); - const k2 = new TestNotebookKernel({ label: 'a', viewType: 'foo' }); - const k3 = new TestNotebookKernel({ label: 'b', viewType: 'foo' }); + const k1 = new TestNotebookKernel({ label: 'z', notebookType: 'foo' }); + const k2 = new TestNotebookKernel({ label: 'a', notebookType: 'foo' }); + const k3 = new TestNotebookKernel({ label: 'b', notebookType: 'foo' }); disposables.add(kernelService.registerKernel(k1)); disposables.add(kernelService.registerKernel(k2)); @@ -158,12 +158,12 @@ suite('NotebookKernelHistoryService', () => { }); const kernelHistoryService = disposables.add(instantiationService.createInstance(NotebookKernelHistoryService)); - let info = kernelHistoryService.getKernels({ uri: u1, viewType: 'foo' }); + let info = kernelHistoryService.getKernels({ uri: u1, notebookType: 'foo' }); assert.equal(info.all.length, 1); assert.deepStrictEqual(info.selected, undefined); kernelHistoryService.addMostRecentKernel(k3); - info = kernelHistoryService.getKernels({ uri: u1, viewType: 'foo' }); + info = kernelHistoryService.getKernels({ uri: u1, notebookType: 'foo' }); assert.deepStrictEqual(info.all, [k3, k2]); }); }); @@ -190,9 +190,9 @@ class TestNotebookKernel implements INotebookKernel { return AsyncIterableObject.EMPTY; } - constructor(opts?: { languages?: string[]; label?: string; viewType?: string }) { + constructor(opts?: { languages?: string[]; label?: string; notebookType?: string }) { this.supportedLanguages = opts?.languages ?? [PLAINTEXT_LANGUAGE_ID]; this.label = opts?.label ?? this.label; - this.viewType = opts?.viewType ?? this.viewType; + this.viewType = opts?.notebookType ?? this.viewType; } } diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookKernelService.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookKernelService.test.ts index cbe5bf90b3b..b05cd81e6a7 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookKernelService.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookKernelService.test.ts @@ -72,7 +72,7 @@ suite('NotebookKernelService', () => { disposables.add(kernelService.registerKernel(k2)); // equal priorities -> sort by name - let info = kernelService.getMatchingKernel({ uri: u1, viewType: 'foo' }); + let info = kernelService.getMatchingKernel({ uri: u1, notebookType: 'foo' }); assert.ok(info.all[0] === k2); assert.ok(info.all[1] === k1); @@ -81,18 +81,18 @@ suite('NotebookKernelService', () => { kernelService.updateKernelNotebookAffinity(k2, u2, 1); // updated - info = kernelService.getMatchingKernel({ uri: u1, viewType: 'foo' }); + info = kernelService.getMatchingKernel({ uri: u1, notebookType: 'foo' }); assert.ok(info.all[0] === k2); assert.ok(info.all[1] === k1); // NOT updated - info = kernelService.getMatchingKernel({ uri: u2, viewType: 'foo' }); + info = kernelService.getMatchingKernel({ uri: u2, notebookType: 'foo' }); assert.ok(info.all[0] === k2); assert.ok(info.all[1] === k1); // reset kernelService.updateKernelNotebookAffinity(k2, u1, undefined); - info = kernelService.getMatchingKernel({ uri: u1, viewType: 'foo' }); + info = kernelService.getMatchingKernel({ uri: u1, notebookType: 'foo' }); assert.ok(info.all[0] === k2); assert.ok(info.all[1] === k1); }); @@ -103,18 +103,18 @@ suite('NotebookKernelService', () => { const kernel = new TestNotebookKernel(); disposables.add(kernelService.registerKernel(kernel)); - let info = kernelService.getMatchingKernel({ uri: notebook, viewType: 'foo' }); + let info = kernelService.getMatchingKernel({ uri: notebook, notebookType: 'foo' }); assert.strictEqual(info.all.length, 1); assert.ok(info.all[0] === kernel); const betterKernel = new TestNotebookKernel(); disposables.add(kernelService.registerKernel(betterKernel)); - info = kernelService.getMatchingKernel({ uri: notebook, viewType: 'foo' }); + info = kernelService.getMatchingKernel({ uri: notebook, notebookType: 'foo' }); assert.strictEqual(info.all.length, 2); kernelService.updateKernelNotebookAffinity(betterKernel, notebook, 2); - info = kernelService.getMatchingKernel({ uri: notebook, viewType: 'foo' }); + info = kernelService.getMatchingKernel({ uri: notebook, notebookType: 'foo' }); assert.strictEqual(info.all.length, 2); assert.ok(info.all[0] === betterKernel); assert.ok(info.all[1] === kernel); @@ -123,8 +123,8 @@ suite('NotebookKernelService', () => { test('onDidChangeSelectedNotebooks not fired on initial notebook open #121904', function () { const uri = URI.parse('foo:///one'); - const jupyter = { uri, viewType: 'jupyter' }; - const dotnet = { uri, viewType: 'dotnet' }; + const jupyter = { uri, viewType: 'jupyter', notebookType: 'jupyter' }; + const dotnet = { uri, viewType: 'dotnet', notebookType: 'dotnet' }; const jupyterKernel = new TestNotebookKernel({ viewType: jupyter.viewType }); const dotnetKernel = new TestNotebookKernel({ viewType: dotnet.viewType }); @@ -144,8 +144,8 @@ suite('NotebookKernelService', () => { test('onDidChangeSelectedNotebooks not fired on initial notebook open #121904, p2', async function () { const uri = URI.parse('foo:///one'); - const jupyter = { uri, viewType: 'jupyter' }; - const dotnet = { uri, viewType: 'dotnet' }; + const jupyter = { uri, viewType: 'jupyter', notebookType: 'jupyter' }; + const dotnet = { uri, viewType: 'dotnet', notebookType: 'dotnet' }; const jupyterKernel = new TestNotebookKernel({ viewType: jupyter.viewType }); const dotnetKernel = new TestNotebookKernel({ viewType: dotnet.viewType }); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookServiceImpl.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookServiceImpl.test.ts index ce634389f93..77ce4841a79 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookServiceImpl.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookServiceImpl.test.ts @@ -56,7 +56,6 @@ suite('NotebookProviderInfoStore', function () { displayName: 'foo', selectors: [{ filenamePattern: '*.foo' }], priority: RegisteredEditorPriority.default, - exclusive: false, providerDisplayName: 'foo', }); const barInfo = new NotebookProviderInfo({ @@ -65,7 +64,6 @@ suite('NotebookProviderInfoStore', function () { displayName: 'bar', selectors: [{ filenamePattern: '*.bar' }], priority: RegisteredEditorPriority.default, - exclusive: false, providerDisplayName: 'bar', }); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts index 1e512e7c1a0..c9d92f4a71b 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts @@ -66,7 +66,7 @@ suite('NotebookViewModel', () => { test('ctor', function () { const notebook = new NotebookTextModel('notebook', URI.parse('test'), [], {}, { transientCellMetadata: {}, transientDocumentMetadata: {}, transientOutputs: false, cellContentMetadata: {} }, undoRedoService, modelService, languageService, languageDetectionService); const model = new NotebookEditorTestModel(notebook); - const options = new NotebookOptions(mainWindow, instantiationService.get(IConfigurationService), instantiationService.get(INotebookExecutionStateService), instantiationService.get(ICodeEditorService), false); + const options = new NotebookOptions(mainWindow, false, undefined, instantiationService.get(IConfigurationService), instantiationService.get(INotebookExecutionStateService), instantiationService.get(ICodeEditorService)); const eventDispatcher = new NotebookEventDispatcher(); const viewContext = new ViewContext(options, eventDispatcher, () => ({} as IBaseCellEditorOptions)); const viewModel = new NotebookViewModel('notebook', model.notebook, viewContext, null, { isReadOnly: false }, instantiationService, bulkEditService, undoRedoService, textModelService, notebookExecutionStateService); diff --git a/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts index 8054c280d1e..ef524745b9d 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts @@ -229,7 +229,7 @@ function _createTestNotebookEditor(instantiationService: TestInstantiationServic }), {}, { transientCellMetadata: {}, transientDocumentMetadata: {}, cellContentMetadata: {}, transientOutputs: false })); const model = disposables.add(new NotebookEditorTestModel(notebook)); - const notebookOptions = disposables.add(new NotebookOptions(mainWindow, instantiationService.get(IConfigurationService), instantiationService.get(INotebookExecutionStateService), instantiationService.get(ICodeEditorService), false)); + const notebookOptions = disposables.add(new NotebookOptions(mainWindow, false, undefined, instantiationService.get(IConfigurationService), instantiationService.get(INotebookExecutionStateService), instantiationService.get(ICodeEditorService))); const baseCellEditorOptions = new class extends mock() { }; const viewContext = new ViewContext(notebookOptions, disposables.add(new NotebookEventDispatcher()), () => baseCellEditorOptions); const viewModel: NotebookViewModel = disposables.add(instantiationService.createInstance(NotebookViewModel, viewType, model.notebook, viewContext, null, { isReadOnly: false })); @@ -471,7 +471,7 @@ export function createNotebookCellList(instantiationService: TestInstantiationSe }; const notebookOptions = !!viewContext ? viewContext.notebookOptions - : disposables.add(new NotebookOptions(mainWindow, instantiationService.get(IConfigurationService), instantiationService.get(INotebookExecutionStateService), instantiationService.get(ICodeEditorService), false)); + : disposables.add(new NotebookOptions(mainWindow, false, undefined, instantiationService.get(IConfigurationService), instantiationService.get(INotebookExecutionStateService), instantiationService.get(ICodeEditorService))); const cellList: NotebookCellList = disposables.add(instantiationService.createInstance( NotebookCellList, 'NotebookCellList', diff --git a/src/vs/workbench/contrib/replNotebook/browser/interactiveEditor.css b/src/vs/workbench/contrib/replNotebook/browser/interactiveEditor.css new file mode 100644 index 00000000000..d43c6a6e98b --- /dev/null +++ b/src/vs/workbench/contrib/replNotebook/browser/interactiveEditor.css @@ -0,0 +1,21 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.interactive-editor .input-cell-container:focus-within .input-editor-container .monaco-editor { + outline: solid 1px var(--vscode-notebook-focusedCellBorder); +} + +.interactive-editor .input-cell-container .input-editor-container .monaco-editor { + outline: solid 1px var(--vscode-notebook-inactiveFocusedCellBorder); +} + +.interactive-editor .input-cell-container .input-focus-indicator { + top: 8px; +} + +.interactive-editor .input-cell-container .monaco-editor-background, +.interactive-editor .input-cell-container .margin-view-overlays { + background-color: var(--vscode-notebook-cellEditorBackground, var(--vscode-editor-background)); +} diff --git a/src/vs/workbench/contrib/replNotebook/browser/media/interactive.css b/src/vs/workbench/contrib/replNotebook/browser/media/interactive.css new file mode 100644 index 00000000000..f0f5cd4821e --- /dev/null +++ b/src/vs/workbench/contrib/replNotebook/browser/media/interactive.css @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.interactive-editor .input-cell-container { + box-sizing: border-box; +} + +.interactive-editor .input-cell-container .input-focus-indicator { + position: absolute; + left: 0px; + height: 19px; +} + +.interactive-editor .input-cell-container .input-focus-indicator::before { + border-left: 3px solid transparent; + border-radius: 2px; + margin-left: 4px; + content: ""; + position: absolute; + width: 0px; + height: 100%; + z-index: 10; + left: 0px; + top: 0px; + height: 100%; +} + +.interactive-editor .input-cell-container .run-button-container { + position: absolute; +} + +.interactive-editor .input-cell-container .run-button-container .monaco-toolbar .actions-container { + justify-content: center; +} diff --git a/src/vs/workbench/contrib/replNotebook/browser/repl.contribution.ts b/src/vs/workbench/contrib/replNotebook/browser/repl.contribution.ts new file mode 100644 index 00000000000..04826105015 --- /dev/null +++ b/src/vs/workbench/contrib/replNotebook/browser/repl.contribution.ts @@ -0,0 +1,260 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { EditorPaneDescriptor, IEditorPaneRegistry } from 'vs/workbench/browser/editor'; +import { EditorExtensions, IEditorFactoryRegistry, IEditorSerializer, IUntypedEditorInput } from 'vs/workbench/common/editor'; +// is one contrib allowed to import from another? +import { parse } from 'vs/base/common/marshalling'; +import { assertType } from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { CellEditType, CellKind, NotebookSetting, NotebookWorkingCopyTypeIdentifier, REPL_EDITOR_ID } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookEditorInputOptions } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; +import { ReplEditor } from 'vs/workbench/contrib/replNotebook/browser/replEditor'; +import { ReplEditorInput } from 'vs/workbench/contrib/replNotebook/browser/replEditorInput'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from 'vs/workbench/common/contributions'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IWorkingCopyIdentifier } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { IWorkingCopyEditorHandler, IWorkingCopyEditorService } from 'vs/workbench/services/workingCopy/common/workingCopyEditorService'; +import { extname, isEqual } from 'vs/base/common/resources'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { localize2 } from 'vs/nls'; +import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { IEditorResolverService, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; +import { Schemas } from 'vs/base/common/network'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; +import { isFalsyOrWhitespace } from 'vs/base/common/strings'; +import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; +import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; +import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits'; +import { IInteractiveHistoryService } from 'vs/workbench/contrib/interactive/browser/interactiveHistoryService'; +import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; +import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; + +type SerializedNotebookEditorData = { resource: URI; preferredResource: URI; viewType: string; options?: NotebookEditorInputOptions }; +class ReplEditorSerializer implements IEditorSerializer { + canSerialize(input: EditorInput): boolean { + return input.typeId === ReplEditorInput.ID; + } + serialize(input: EditorInput): string { + assertType(input instanceof ReplEditorInput); + const data: SerializedNotebookEditorData = { + resource: input.resource, + preferredResource: input.preferredResource, + viewType: input.viewType, + options: input.options + }; + return JSON.stringify(data); + } + deserialize(instantiationService: IInstantiationService, raw: string) { + const data = parse(raw); + if (!data) { + return undefined; + } + const { resource, viewType } = data; + if (!data || !URI.isUri(resource) || typeof viewType !== 'string') { + return undefined; + } + + const input = instantiationService.createInstance(ReplEditorInput, resource); + return input; + } +} + +Registry.as(EditorExtensions.EditorPane).registerEditorPane( + EditorPaneDescriptor.create( + ReplEditor, + REPL_EDITOR_ID, + 'REPL Editor' + ), + [ + new SyncDescriptor(ReplEditorInput) + ] +); + +Registry.as(EditorExtensions.EditorFactory).registerEditorSerializer( + ReplEditorInput.ID, + ReplEditorSerializer +); + +export class ReplDocumentContribution extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'workbench.contrib.replDocument'; + + constructor( + @INotebookService notebookService: INotebookService, + @IEditorResolverService editorResolverService: IEditorResolverService, + @IEditorService editorService: IEditorService, + @INotebookEditorModelResolverService private readonly notebookEditorModelResolverService: INotebookEditorModelResolverService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IConfigurationService private readonly configurationService: IConfigurationService + ) { + super(); + + editorResolverService.registerEditor( + `*.replNotebook`, + { + id: 'repl', + label: 'repl Editor', + priority: RegisteredEditorPriority.option + }, + { + canSupportResource: uri => + (uri.scheme === Schemas.untitled && extname(uri) === '.replNotebook') || + (uri.scheme === Schemas.vscodeNotebookCell && extname(uri) === '.replNotebook'), + singlePerResource: true + }, + { + createUntitledEditorInput: async ({ resource, options }) => { + const scratchpad = this.configurationService.getValue(NotebookSetting.InteractiveWindowPromptToSave) !== true; + const ref = await this.notebookEditorModelResolverService.resolve({ untitledResource: resource }, 'jupyter-notebook', { scratchpad }); + + // untitled notebooks are disposed when they get saved. we should not hold a reference + // to such a disposed notebook and therefore dispose the reference as well + ref.object.notebook.onWillDispose(() => { + ref.dispose(); + }); + return { editor: this.instantiationService.createInstance(ReplEditorInput, resource!), options }; + } + } + ); + } +} + +class ReplWindowWorkingCopyEditorHandler extends Disposable implements IWorkbenchContribution, IWorkingCopyEditorHandler { + + static readonly ID = 'workbench.contrib.replWorkingCopyEditorHandler'; + + constructor( + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IWorkingCopyEditorService private readonly workingCopyEditorService: IWorkingCopyEditorService, + @IExtensionService private readonly extensionService: IExtensionService, + ) { + super(); + + this._installHandler(); + } + + handles(workingCopy: IWorkingCopyIdentifier): boolean { + const viewType = this._getViewType(workingCopy); + return !!viewType && viewType === 'jupyter-notebook' && extname(workingCopy.resource) === '.replNotebook'; + + } + + isOpen(workingCopy: IWorkingCopyIdentifier, editor: EditorInput): boolean { + if (!this.handles(workingCopy)) { + return false; + } + + return editor instanceof ReplEditorInput && isEqual(workingCopy.resource, editor.resource); + } + + createEditor(workingCopy: IWorkingCopyIdentifier): EditorInput { + return this.instantiationService.createInstance(ReplEditorInput, workingCopy.resource); + } + + private async _installHandler(): Promise { + await this.extensionService.whenInstalledExtensionsRegistered(); + + this._register(this.workingCopyEditorService.registerHandler(this)); + } + + private _getViewType(workingCopy: IWorkingCopyIdentifier): string | undefined { + return NotebookWorkingCopyTypeIdentifier.parse(workingCopy.typeId); + } +} + +registerWorkbenchContribution2(ReplWindowWorkingCopyEditorHandler.ID, ReplWindowWorkingCopyEditorHandler, WorkbenchPhase.BlockRestore); +registerWorkbenchContribution2(ReplDocumentContribution.ID, ReplDocumentContribution, WorkbenchPhase.BlockRestore); + + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'repl.newRepl', + title: localize2('repl.editor.open', 'New REPL Editor'), + category: 'Create', + }); + } + + async run(accessor: ServicesAccessor) { + const resource = URI.from({ scheme: Schemas.untitled, path: 'repl.replNotebook' }); + const editorInput: IUntypedEditorInput = { resource, options: { override: 'repl' } }; + + const editorService = accessor.get(IEditorService); + await editorService.openEditor(editorInput, 1); + } +}); + +export async function executeReplInput(accessor: ServicesAccessor, editorControl: { notebookEditor: NotebookEditorWidget | undefined; codeEditor: CodeEditorWidget }) { + const bulkEditService = accessor.get(IBulkEditService); + const historyService = accessor.get(IInteractiveHistoryService); + const notebookEditorService = accessor.get(INotebookEditorService); + + if (editorControl && editorControl.notebookEditor && editorControl.codeEditor) { + const notebookDocument = editorControl.notebookEditor.textModel; + const textModel = editorControl.codeEditor.getModel(); + const activeKernel = editorControl.notebookEditor.activeKernel; + const language = activeKernel?.supportedLanguages[0] ?? PLAINTEXT_LANGUAGE_ID; + + if (notebookDocument && textModel) { + const index = notebookDocument.length - 1; + const value = textModel.getValue(); + + if (isFalsyOrWhitespace(value)) { + return; + } + + historyService.addToHistory(notebookDocument.uri, value); + textModel.setValue(''); + notebookDocument.cells[index].resetTextBuffer(textModel.getTextBuffer()); + + const collapseState = editorControl.notebookEditor.notebookOptions.getDisplayOptions().interactiveWindowCollapseCodeCells === 'fromEditor' ? + { + inputCollapsed: false, + outputCollapsed: false + } : + undefined; + + await bulkEditService.apply([ + new ResourceNotebookCellEdit(notebookDocument.uri, + { + editType: CellEditType.Replace, + index: index, + count: 0, + cells: [{ + cellKind: CellKind.Code, + mime: undefined, + language, + source: value, + outputs: [], + metadata: {}, + collapseState + }] + } + ) + ]); + + // reveal the cell into view first + const range = { start: index, end: index + 1 }; + editorControl.notebookEditor.revealCellRangeInView(range); + await editorControl.notebookEditor.executeNotebookCells(editorControl.notebookEditor.getCellsInRange({ start: index, end: index + 1 })); + + // update the selection and focus in the extension host model + const editor = notebookEditorService.getNotebookEditor(editorControl.notebookEditor.getId()); + if (editor) { + editor.setSelections([range]); + editor.setFocus(range); + } + } + } +} diff --git a/src/vs/workbench/contrib/replNotebook/browser/replEditor.ts b/src/vs/workbench/contrib/replNotebook/browser/replEditor.ts new file mode 100644 index 00000000000..67a803eab40 --- /dev/null +++ b/src/vs/workbench/contrib/replNotebook/browser/replEditor.ts @@ -0,0 +1,725 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./media/interactive'; +import * as nls from 'vs/nls'; +import * as DOM from 'vs/base/browser/dom'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Emitter, Event } from 'vs/base/common/event'; +import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; +import { ICodeEditorViewState, IDecorationOptions } from 'vs/editor/common/editorCommon'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { editorForeground, resolveColorValue } from 'vs/platform/theme/common/colorRegistry'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; +import { EditorPaneSelectionChangeReason, IEditorMemento, IEditorOpenContext, IEditorPaneScrollPosition, IEditorPaneSelectionChangeEvent, IEditorPaneWithScrolling } from 'vs/workbench/common/editor'; +import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; +import { ICellViewModel, INotebookEditorOptions, INotebookEditorViewState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookEditorExtensionsRegistry } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; +import { IBorrowValue, INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService'; +import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; +import { GroupsOrder, IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { ExecutionStateCellStatusBarContrib, TimerCellStatusBarContrib } from 'vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController'; +import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { InteractiveWindowSetting, INTERACTIVE_INPUT_CURSOR_BOUNDARY } from 'vs/workbench/contrib/interactive/browser/interactiveCommon'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { NotebookOptions } from 'vs/workbench/contrib/notebook/browser/notebookOptions'; +import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { createActionViewItem, createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IAction } from 'vs/base/common/actions'; +import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; +import { ParameterHintsController } from 'vs/editor/contrib/parameterHints/browser/parameterHints'; +import { MenuPreventer } from 'vs/workbench/contrib/codeEditor/browser/menuPreventer'; +import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard'; +import { ContextMenuController } from 'vs/editor/contrib/contextmenu/browser/contextmenu'; +import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController'; +import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2'; +import { TabCompletionController } from 'vs/workbench/contrib/snippets/browser/tabCompletion'; +import { MarkerController } from 'vs/editor/contrib/gotoError/browser/gotoError'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; +import { ITextEditorOptions, TextEditorSelectionSource } from 'vs/platform/editor/common/editor'; +import { INotebookExecutionStateService, NotebookExecutionType } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; +import { NOTEBOOK_KERNEL } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { ICursorPositionChangedEvent } from 'vs/editor/common/cursorEvents'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { isEqual } from 'vs/base/common/resources'; +import { NotebookFindContrib } from 'vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget'; +import { EXECUTE_REPL_COMMAND_ID, REPL_EDITOR_ID } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import 'vs/css!./interactiveEditor'; +import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { deepClone } from 'vs/base/common/objects'; +import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; +import { ReplEditorInput } from 'vs/workbench/contrib/replNotebook/browser/replEditorInput'; + +const DECORATION_KEY = 'interactiveInputDecoration'; +const INTERACTIVE_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'InteractiveEditorViewState'; + +const INPUT_CELL_VERTICAL_PADDING = 8; +const INPUT_CELL_HORIZONTAL_PADDING_RIGHT = 10; +const INPUT_EDITOR_PADDING = 8; + +export interface InteractiveEditorViewState { + readonly notebook?: INotebookEditorViewState; + readonly input?: ICodeEditorViewState | null; +} + +export interface InteractiveEditorOptions extends ITextEditorOptions { + readonly viewState?: InteractiveEditorViewState; +} + +export class ReplEditor extends EditorPane implements IEditorPaneWithScrolling { + private _rootElement!: HTMLElement; + private _styleElement!: HTMLStyleElement; + private _notebookEditorContainer!: HTMLElement; + private _notebookWidget: IBorrowValue = { value: undefined }; + private _inputCellContainer!: HTMLElement; + private _inputFocusIndicator!: HTMLElement; + private _inputRunButtonContainer!: HTMLElement; + private _inputEditorContainer!: HTMLElement; + private _codeEditorWidget!: CodeEditorWidget; + private _notebookWidgetService: INotebookEditorService; + private _instantiationService: IInstantiationService; + private _languageService: ILanguageService; + private _contextKeyService: IContextKeyService; + private _configurationService: IConfigurationService; + private _notebookKernelService: INotebookKernelService; + private _keybindingService: IKeybindingService; + private _menuService: IMenuService; + private _contextMenuService: IContextMenuService; + private _editorGroupService: IEditorGroupsService; + private _extensionService: IExtensionService; + private readonly _widgetDisposableStore: DisposableStore = this._register(new DisposableStore()); + private _lastLayoutDimensions?: { readonly dimension: DOM.Dimension; readonly position: DOM.IDomPosition }; + private _editorOptions: IEditorOptions; + private _notebookOptions: NotebookOptions; + private _editorMemento: IEditorMemento; + private readonly _groupListener = this._register(new MutableDisposable()); + private _runbuttonToolbar: ToolBar | undefined; + + private _onDidFocusWidget = this._register(new Emitter()); + override get onDidFocus(): Event { return this._onDidFocusWidget.event; } + private _onDidChangeSelection = this._register(new Emitter()); + readonly onDidChangeSelection = this._onDidChangeSelection.event; + private _onDidChangeScroll = this._register(new Emitter()); + readonly onDidChangeScroll = this._onDidChangeScroll.event; + + constructor( + group: IEditorGroup, + @ITelemetryService telemetryService: ITelemetryService, + @IThemeService themeService: IThemeService, + @IStorageService storageService: IStorageService, + @IInstantiationService instantiationService: IInstantiationService, + @INotebookEditorService notebookWidgetService: INotebookEditorService, + @IContextKeyService contextKeyService: IContextKeyService, + @ICodeEditorService codeEditorService: ICodeEditorService, + @INotebookKernelService notebookKernelService: INotebookKernelService, + @ILanguageService languageService: ILanguageService, + @IKeybindingService keybindingService: IKeybindingService, + @IConfigurationService configurationService: IConfigurationService, + @IMenuService menuService: IMenuService, + @IContextMenuService contextMenuService: IContextMenuService, + @IEditorGroupsService editorGroupService: IEditorGroupsService, + @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService, + @INotebookExecutionStateService notebookExecutionStateService: INotebookExecutionStateService, + @IExtensionService extensionService: IExtensionService, + ) { + super( + REPL_EDITOR_ID, + group, + telemetryService, + themeService, + storageService + ); + this._instantiationService = instantiationService; + this._notebookWidgetService = notebookWidgetService; + this._contextKeyService = contextKeyService; + this._configurationService = configurationService; + this._notebookKernelService = notebookKernelService; + this._languageService = languageService; + this._keybindingService = keybindingService; + this._menuService = menuService; + this._contextMenuService = contextMenuService; + this._editorGroupService = editorGroupService; + this._extensionService = extensionService; + + this._editorOptions = this._computeEditorOptions(); + this._register(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('editor') || e.affectsConfiguration('notebook')) { + this._editorOptions = this._computeEditorOptions(); + } + })); + this._notebookOptions = instantiationService.createInstance(NotebookOptions, this.window, true, { cellToolbarInteraction: 'hover', globalToolbar: true, stickyScrollEnabled: false, dragAndDropEnabled: false }); + this._editorMemento = this.getEditorMemento(editorGroupService, textResourceConfigurationService, INTERACTIVE_EDITOR_VIEW_STATE_PREFERENCE_KEY); + + codeEditorService.registerDecorationType('interactive-decoration', DECORATION_KEY, {}); + this._register(this._keybindingService.onDidUpdateKeybindings(this._updateInputDecoration, this)); + this._register(notebookExecutionStateService.onDidChangeExecution((e) => { + if (e.type === NotebookExecutionType.cell && isEqual(e.notebook, this._notebookWidget.value?.viewModel?.notebookDocument.uri)) { + const cell = this._notebookWidget.value?.getCellByHandle(e.cellHandle); + if (cell && e.changed?.state) { + this._scrollIfNecessary(cell); + } + } + })); + } + + private get inputCellContainerHeight() { + return 19 + 2 + INPUT_CELL_VERTICAL_PADDING * 2 + INPUT_EDITOR_PADDING * 2; + } + + private get inputCellEditorHeight() { + return 19 + INPUT_EDITOR_PADDING * 2; + } + + protected createEditor(parent: HTMLElement): void { + this._rootElement = DOM.append(parent, DOM.$('.interactive-editor')); + this._rootElement.style.position = 'relative'; + this._notebookEditorContainer = DOM.append(this._rootElement, DOM.$('.notebook-editor-container')); + this._inputCellContainer = DOM.append(this._rootElement, DOM.$('.input-cell-container')); + this._inputCellContainer.style.position = 'absolute'; + this._inputCellContainer.style.height = `${this.inputCellContainerHeight}px`; + this._inputFocusIndicator = DOM.append(this._inputCellContainer, DOM.$('.input-focus-indicator')); + this._inputRunButtonContainer = DOM.append(this._inputCellContainer, DOM.$('.run-button-container')); + this._setupRunButtonToolbar(this._inputRunButtonContainer); + this._inputEditorContainer = DOM.append(this._inputCellContainer, DOM.$('.input-editor-container')); + this._createLayoutStyles(); + } + + private _setupRunButtonToolbar(runButtonContainer: HTMLElement) { + const menu = this._register(this._menuService.createMenu(MenuId.ReplInputExecute, this._contextKeyService)); + this._runbuttonToolbar = this._register(new ToolBar(runButtonContainer, this._contextMenuService, { + getKeyBinding: action => this._keybindingService.lookupKeybinding(action.id), + actionViewItemProvider: (action, options) => { + return createActionViewItem(this._instantiationService, action, options); + }, + renderDropdownAsChildElement: true + })); + + const primary: IAction[] = []; + const secondary: IAction[] = []; + const result = { primary, secondary }; + + createAndFillInActionBarActions(menu, { shouldForwardArgs: true }, result); + this._runbuttonToolbar.setActions([...primary, ...secondary]); + } + + private _createLayoutStyles(): void { + this._styleElement = DOM.createStyleSheet(this._rootElement); + const styleSheets: string[] = []; + + const { + codeCellLeftMargin, + cellRunGutter + } = this._notebookOptions.getLayoutConfiguration(); + const { + focusIndicator + } = this._notebookOptions.getDisplayOptions(); + const leftMargin = this._notebookOptions.getCellEditorContainerLeftMargin(); + + styleSheets.push(` + .interactive-editor .input-cell-container { + padding: ${INPUT_CELL_VERTICAL_PADDING}px ${INPUT_CELL_HORIZONTAL_PADDING_RIGHT}px ${INPUT_CELL_VERTICAL_PADDING}px ${leftMargin}px; + } + `); + if (focusIndicator === 'gutter') { + styleSheets.push(` + .interactive-editor .input-cell-container:focus-within .input-focus-indicator::before { + border-color: var(--vscode-notebook-focusedCellBorder) !important; + } + .interactive-editor .input-focus-indicator::before { + border-color: var(--vscode-notebook-inactiveFocusedCellBorder) !important; + } + .interactive-editor .input-cell-container .input-focus-indicator { + display: block; + top: ${INPUT_CELL_VERTICAL_PADDING}px; + } + .interactive-editor .input-cell-container { + border-top: 1px solid var(--vscode-notebook-inactiveFocusedCellBorder); + } + `); + } else { + // border + styleSheets.push(` + .interactive-editor .input-cell-container { + border-top: 1px solid var(--vscode-notebook-inactiveFocusedCellBorder); + } + .interactive-editor .input-cell-container .input-focus-indicator { + display: none; + } + `); + } + + styleSheets.push(` + .interactive-editor .input-cell-container .run-button-container { + width: ${cellRunGutter}px; + left: ${codeCellLeftMargin}px; + margin-top: ${INPUT_EDITOR_PADDING - 2}px; + } + `); + + this._styleElement.textContent = styleSheets.join('\n'); + } + + private _computeEditorOptions(): IEditorOptions { + let overrideIdentifier: string | undefined = undefined; + if (this._codeEditorWidget) { + overrideIdentifier = this._codeEditorWidget.getModel()?.getLanguageId(); + } + const editorOptions = deepClone(this._configurationService.getValue('editor', { overrideIdentifier })); + const editorOptionsOverride = getSimpleEditorOptions(this._configurationService); + const computed = Object.freeze({ + ...editorOptions, + ...editorOptionsOverride, + ...{ + glyphMargin: true, + padding: { + top: INPUT_EDITOR_PADDING, + bottom: INPUT_EDITOR_PADDING + }, + hover: { + enabled: true + } + } + }); + + return computed; + } + + protected override saveState(): void { + this._saveEditorViewState(this.input); + super.saveState(); + } + + override getViewState(): InteractiveEditorViewState | undefined { + const input = this.input; + if (!(input instanceof ReplEditorInput)) { + return undefined; + } + + this._saveEditorViewState(input); + return this._loadNotebookEditorViewState(input); + } + + private _saveEditorViewState(input: EditorInput | undefined): void { + if (this._notebookWidget.value && input instanceof ReplEditorInput) { + if (this._notebookWidget.value.isDisposed) { + return; + } + + const state = this._notebookWidget.value.getEditorViewState(); + const editorState = this._codeEditorWidget.saveViewState(); + this._editorMemento.saveEditorState(this.group, input.resource, { + notebook: state, + input: editorState + }); + } + } + + private _loadNotebookEditorViewState(input: ReplEditorInput): InteractiveEditorViewState | undefined { + const result = this._editorMemento.loadEditorState(this.group, input.resource); + if (result) { + return result; + } + // when we don't have a view state for the group/input-tuple then we try to use an existing + // editor for the same resource. + for (const group of this._editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE)) { + if (group.activeEditorPane !== this && group.activeEditorPane === this && group.activeEditor?.matches(input)) { + const notebook = this._notebookWidget.value?.getEditorViewState(); + const input = this._codeEditorWidget.saveViewState(); + return { + notebook, + input + }; + } + } + return; + } + + override async setInput(input: ReplEditorInput, options: InteractiveEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + // there currently is a widget which we still own so + // we need to hide it before getting a new widget + this._notebookWidget.value?.onWillHide(); + + this._codeEditorWidget?.dispose(); + + this._widgetDisposableStore.clear(); + + this._notebookWidget = >this._instantiationService.invokeFunction(this._notebookWidgetService.retrieveWidget, this.group, input, { + isEmbedded: true, + isReadOnly: true, + forRepl: true, + contributions: NotebookEditorExtensionsRegistry.getSomeEditorContributions([ + ExecutionStateCellStatusBarContrib.id, + TimerCellStatusBarContrib.id, + NotebookFindContrib.id + ]), + menuIds: { + notebookToolbar: MenuId.InteractiveToolbar, + cellTitleToolbar: MenuId.InteractiveCellTitle, + cellDeleteToolbar: MenuId.InteractiveCellDelete, + cellInsertToolbar: MenuId.NotebookCellBetween, + cellTopInsertToolbar: MenuId.NotebookCellListTop, + cellExecuteToolbar: MenuId.InteractiveCellExecute, + cellExecutePrimary: undefined + }, + cellEditorContributions: EditorExtensionsRegistry.getSomeEditorContributions([ + SelectionClipboardContributionID, + ContextMenuController.ID, + HoverController.ID, + MarkerController.ID + ]), + options: this._notebookOptions, + codeWindow: this.window + }, undefined, this.window); + + this._codeEditorWidget = this._instantiationService.createInstance(CodeEditorWidget, this._inputEditorContainer, this._editorOptions, { + ...{ + isSimpleWidget: false, + contributions: EditorExtensionsRegistry.getSomeEditorContributions([ + MenuPreventer.ID, + SelectionClipboardContributionID, + ContextMenuController.ID, + SuggestController.ID, + ParameterHintsController.ID, + SnippetController2.ID, + TabCompletionController.ID, + HoverController.ID, + MarkerController.ID + ]) + } + }); + + if (this._lastLayoutDimensions) { + this._notebookEditorContainer.style.height = `${this._lastLayoutDimensions.dimension.height - this.inputCellContainerHeight}px`; + this._notebookWidget.value!.layout(new DOM.Dimension(this._lastLayoutDimensions.dimension.width, this._lastLayoutDimensions.dimension.height - this.inputCellContainerHeight), this._notebookEditorContainer); + const leftMargin = this._notebookOptions.getCellEditorContainerLeftMargin(); + const maxHeight = Math.min(this._lastLayoutDimensions.dimension.height / 2, this.inputCellEditorHeight); + this._codeEditorWidget.layout(this._validateDimension(this._lastLayoutDimensions.dimension.width - leftMargin - INPUT_CELL_HORIZONTAL_PADDING_RIGHT, maxHeight)); + this._inputFocusIndicator.style.height = `${this.inputCellEditorHeight}px`; + this._inputCellContainer.style.top = `${this._lastLayoutDimensions.dimension.height - this.inputCellContainerHeight}px`; + this._inputCellContainer.style.width = `${this._lastLayoutDimensions.dimension.width}px`; + } + + await super.setInput(input, options, context, token); + const model = await input.resolve(); + if (this._runbuttonToolbar) { + this._runbuttonToolbar.context = input.resource; + } + + if (model === null) { + throw new Error('The REPL model could not be resolved'); + } + + this._notebookWidget.value?.setParentContextKeyService(this._contextKeyService); + + const viewState = options?.viewState ?? this._loadNotebookEditorViewState(input); + await this._extensionService.whenInstalledExtensionsRegistered(); + await this._notebookWidget.value!.setModel(model.notebook, viewState?.notebook); + model.notebook.setCellCollapseDefault(this._notebookOptions.getCellCollapseDefault()); + this._notebookWidget.value!.setOptions({ + isReadOnly: true + }); + this._widgetDisposableStore.add(this._notebookWidget.value!.onDidResizeOutput((cvm) => { + this._scrollIfNecessary(cvm); + })); + this._widgetDisposableStore.add(this._notebookWidget.value!.onDidFocusWidget(() => this._onDidFocusWidget.fire())); + this._widgetDisposableStore.add(this._notebookOptions.onDidChangeOptions(e => { + if (e.compactView || e.focusIndicator) { + // update the styling + this._styleElement?.remove(); + this._createLayoutStyles(); + } + + if (this._lastLayoutDimensions && this.isVisible()) { + this.layout(this._lastLayoutDimensions.dimension, this._lastLayoutDimensions.position); + } + + if (e.interactiveWindowCollapseCodeCells) { + model.notebook.setCellCollapseDefault(this._notebookOptions.getCellCollapseDefault()); + } + })); + + const editorModel = await input.resolveInput(model.notebook); + this._codeEditorWidget.setModel(editorModel); + if (viewState?.input) { + this._codeEditorWidget.restoreViewState(viewState.input); + } + this._editorOptions = this._computeEditorOptions(); + this._codeEditorWidget.updateOptions(this._editorOptions); + + this._widgetDisposableStore.add(this._codeEditorWidget.onDidFocusEditorWidget(() => this._onDidFocusWidget.fire())); + this._widgetDisposableStore.add(this._codeEditorWidget.onDidContentSizeChange(e => { + if (!e.contentHeightChanged) { + return; + } + + if (this._lastLayoutDimensions) { + this._layoutWidgets(this._lastLayoutDimensions.dimension, this._lastLayoutDimensions.position); + } + })); + + this._widgetDisposableStore.add(this._codeEditorWidget.onDidChangeCursorPosition(e => this._onDidChangeSelection.fire({ reason: this._toEditorPaneSelectionChangeReason(e) }))); + this._widgetDisposableStore.add(this._codeEditorWidget.onDidChangeModelContent(() => this._onDidChangeSelection.fire({ reason: EditorPaneSelectionChangeReason.EDIT }))); + + + this._widgetDisposableStore.add(this._notebookKernelService.onDidChangeNotebookAffinity(this._syncWithKernel, this)); + this._widgetDisposableStore.add(this._notebookKernelService.onDidChangeSelectedNotebooks(this._syncWithKernel, this)); + + this._widgetDisposableStore.add(this.themeService.onDidColorThemeChange(() => { + if (this.isVisible()) { + this._updateInputDecoration(); + } + })); + + this._widgetDisposableStore.add(this._codeEditorWidget.onDidChangeModelContent(() => { + if (this.isVisible()) { + this._updateInputDecoration(); + } + })); + + const cursorAtBoundaryContext = INTERACTIVE_INPUT_CURSOR_BOUNDARY.bindTo(this._contextKeyService); + if (input.resource && input.historyService.has(input.resource)) { + cursorAtBoundaryContext.set('top'); + } else { + cursorAtBoundaryContext.set('none'); + } + + this._widgetDisposableStore.add(this._codeEditorWidget.onDidChangeCursorPosition(({ position }) => { + const viewModel = this._codeEditorWidget._getViewModel()!; + const lastLineNumber = viewModel.getLineCount(); + const lastLineCol = viewModel.getLineLength(lastLineNumber) + 1; + const viewPosition = viewModel.coordinatesConverter.convertModelPositionToViewPosition(position); + const firstLine = viewPosition.lineNumber === 1 && viewPosition.column === 1; + const lastLine = viewPosition.lineNumber === lastLineNumber && viewPosition.column === lastLineCol; + + if (firstLine) { + if (lastLine) { + cursorAtBoundaryContext.set('both'); + } else { + cursorAtBoundaryContext.set('top'); + } + } else { + if (lastLine) { + cursorAtBoundaryContext.set('bottom'); + } else { + cursorAtBoundaryContext.set('none'); + } + } + })); + + this._widgetDisposableStore.add(editorModel.onDidChangeContent(() => { + const value = editorModel.getValue(); + if (this.input?.resource && value !== '') { + (this.input as ReplEditorInput).historyService.replaceLast(this.input.resource, value); + } + })); + + this._widgetDisposableStore.add(this._notebookWidget.value!.onDidScroll(() => this._onDidChangeScroll.fire())); + + this._syncWithKernel(); + } + + override setOptions(options: INotebookEditorOptions | undefined): void { + this._notebookWidget.value?.setOptions(options); + super.setOptions(options); + } + + private _toEditorPaneSelectionChangeReason(e: ICursorPositionChangedEvent): EditorPaneSelectionChangeReason { + switch (e.source) { + case TextEditorSelectionSource.PROGRAMMATIC: return EditorPaneSelectionChangeReason.PROGRAMMATIC; + case TextEditorSelectionSource.NAVIGATION: return EditorPaneSelectionChangeReason.NAVIGATION; + case TextEditorSelectionSource.JUMP: return EditorPaneSelectionChangeReason.JUMP; + default: return EditorPaneSelectionChangeReason.USER; + } + } + + private _cellAtBottom(cell: ICellViewModel): boolean { + const visibleRanges = this._notebookWidget.value?.visibleRanges || []; + const cellIndex = this._notebookWidget.value?.getCellIndex(cell); + if (cellIndex === Math.max(...visibleRanges.map(range => range.end - 1))) { + return true; + } + return false; + } + + private _scrollIfNecessary(cvm: ICellViewModel) { + const index = this._notebookWidget.value!.getCellIndex(cvm); + if (index === this._notebookWidget.value!.getLength() - 1) { + // If we're already at the bottom or auto scroll is enabled, scroll to the bottom + if (this._configurationService.getValue(InteractiveWindowSetting.interactiveWindowAlwaysScrollOnNewCell) || this._cellAtBottom(cvm)) { + this._notebookWidget.value!.scrollToBottom(); + } + } + } + + private _syncWithKernel() { + const notebook = this._notebookWidget.value?.textModel; + const textModel = this._codeEditorWidget.getModel(); + + if (notebook && textModel) { + const info = this._notebookKernelService.getMatchingKernel(notebook); + const selectedOrSuggested = info.selected + ?? (info.suggestions.length === 1 ? info.suggestions[0] : undefined) + ?? (info.all.length === 1 ? info.all[0] : undefined); + + if (selectedOrSuggested) { + const language = selectedOrSuggested.supportedLanguages[0]; + // All kernels will initially list plaintext as the supported language before they properly initialized. + if (language && language !== 'plaintext') { + const newMode = this._languageService.createById(language).languageId; + textModel.setLanguage(newMode); + } + + NOTEBOOK_KERNEL.bindTo(this._contextKeyService).set(selectedOrSuggested.id); + } + } + + this._updateInputDecoration(); + } + + layout(dimension: DOM.Dimension, position: DOM.IDomPosition): void { + this._rootElement.classList.toggle('mid-width', dimension.width < 1000 && dimension.width >= 600); + this._rootElement.classList.toggle('narrow-width', dimension.width < 600); + const editorHeightChanged = dimension.height !== this._lastLayoutDimensions?.dimension.height; + this._lastLayoutDimensions = { dimension, position }; + + if (!this._notebookWidget.value) { + return; + } + + if (editorHeightChanged && this._codeEditorWidget) { + SuggestController.get(this._codeEditorWidget)?.cancelSuggestWidget(); + } + + this._notebookEditorContainer.style.height = `${this._lastLayoutDimensions.dimension.height - this.inputCellContainerHeight}px`; + this._layoutWidgets(dimension, position); + } + + private _layoutWidgets(dimension: DOM.Dimension, position: DOM.IDomPosition) { + const contentHeight = this._codeEditorWidget.hasModel() ? this._codeEditorWidget.getContentHeight() : this.inputCellEditorHeight; + const maxHeight = Math.min(dimension.height / 2, contentHeight); + const leftMargin = this._notebookOptions.getCellEditorContainerLeftMargin(); + + const inputCellContainerHeight = maxHeight + INPUT_CELL_VERTICAL_PADDING * 2; + this._notebookEditorContainer.style.height = `${dimension.height - inputCellContainerHeight}px`; + + this._notebookWidget.value!.layout(dimension.with(dimension.width, dimension.height - inputCellContainerHeight), this._notebookEditorContainer, position); + this._codeEditorWidget.layout(this._validateDimension(dimension.width - leftMargin - INPUT_CELL_HORIZONTAL_PADDING_RIGHT, maxHeight)); + this._inputFocusIndicator.style.height = `${contentHeight}px`; + this._inputCellContainer.style.top = `${dimension.height - inputCellContainerHeight}px`; + this._inputCellContainer.style.width = `${dimension.width}px`; + } + + private _validateDimension(width: number, height: number) { + return new DOM.Dimension(Math.max(0, width), Math.max(0, height)); + } + + private _updateInputDecoration(): void { + if (!this._codeEditorWidget) { + return; + } + + if (!this._codeEditorWidget.hasModel()) { + return; + } + + const model = this._codeEditorWidget.getModel(); + + const decorations: IDecorationOptions[] = []; + + if (model?.getValueLength() === 0) { + const transparentForeground = resolveColorValue(editorForeground, this.themeService.getColorTheme())?.transparent(0.4); + const languageId = model.getLanguageId(); + if (languageId !== 'plaintext') { + const keybinding = this._keybindingService.lookupKeybinding(EXECUTE_REPL_COMMAND_ID, this._contextKeyService)?.getLabel(); + const text = keybinding ? + nls.localize('interactiveInputPlaceHolder', "Type '{0}' code here and press {1} to run", languageId, keybinding) : + nls.localize('interactiveInputPlaceHolderNoKeybinding', "Type '{0}' code here and click run", languageId); + decorations.push({ + range: { + startLineNumber: 0, + endLineNumber: 0, + startColumn: 0, + endColumn: 1 + }, + renderOptions: { + after: { + contentText: text, + color: transparentForeground ? transparentForeground.toString() : undefined + } + } + }); + } + + } + + this._codeEditorWidget.setDecorationsByType('interactive-decoration', DECORATION_KEY, decorations); + } + + getScrollPosition(): IEditorPaneScrollPosition { + return { + scrollTop: this._notebookWidget.value?.scrollTop ?? 0, + scrollLeft: 0 + }; + } + + setScrollPosition(position: IEditorPaneScrollPosition): void { + this._notebookWidget.value?.setScrollTop(position.scrollTop); + } + + override focus() { + super.focus(); + + this._notebookWidget.value?.onShow(); + this._codeEditorWidget.focus(); + } + + focusHistory() { + this._notebookWidget.value!.focus(); + } + + protected override setEditorVisible(visible: boolean): void { + super.setEditorVisible(visible); + this._groupListener.value = this.group.onWillCloseEditor(e => this._saveEditorViewState(e.editor)); + + if (!visible) { + this._saveEditorViewState(this.input); + if (this.input && this._notebookWidget.value) { + this._notebookWidget.value.onWillHide(); + } + } + } + + override clearInput() { + if (this._notebookWidget.value) { + this._saveEditorViewState(this.input); + this._notebookWidget.value.onWillHide(); + } + + this._codeEditorWidget?.dispose(); + + this._notebookWidget = { value: undefined }; + this._widgetDisposableStore.clear(); + + super.clearInput(); + } + + override getControl(): { notebookEditor: NotebookEditorWidget | undefined; codeEditor: CodeEditorWidget } { + return { + notebookEditor: this._notebookWidget.value, + codeEditor: this._codeEditorWidget + }; + } +} diff --git a/src/vs/workbench/contrib/replNotebook/browser/replEditorInput.ts b/src/vs/workbench/contrib/replNotebook/browser/replEditorInput.ts new file mode 100644 index 00000000000..bf82d590a26 --- /dev/null +++ b/src/vs/workbench/contrib/replNotebook/browser/replEditorInput.ts @@ -0,0 +1,88 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IReference } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; +import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IFileService } from 'vs/platform/files/common/files'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { EditorInputCapabilities } from 'vs/workbench/common/editor'; +import { IInteractiveHistoryService } from 'vs/workbench/contrib/interactive/browser/interactiveHistoryService'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; +import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { ICustomEditorLabelService } from 'vs/workbench/services/editor/common/customEditorLabelService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; + +export class ReplEditorInput extends NotebookEditorInput { + static override ID: string = 'workbench.editorinputs.replEditorInput'; + + private inputModelRef: IReference | undefined; + private isScratchpad: boolean; + private isDisposing = false; + + constructor( + resource: URI, + @INotebookService _notebookService: INotebookService, + @INotebookEditorModelResolverService _notebookModelResolverService: INotebookEditorModelResolverService, + @IFileDialogService _fileDialogService: IFileDialogService, + @ILabelService labelService: ILabelService, + @IFileService fileService: IFileService, + @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService, + @IExtensionService extensionService: IExtensionService, + @IEditorService editorService: IEditorService, + @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService, + @ICustomEditorLabelService customEditorLabelService: ICustomEditorLabelService, + @IInteractiveHistoryService public readonly historyService: IInteractiveHistoryService, + @ITextModelService private readonly _textModelService: ITextModelService, + @IConfigurationService configurationService: IConfigurationService + ) { + super(resource, undefined, 'jupyter-notebook', {}, _notebookService, _notebookModelResolverService, _fileDialogService, labelService, fileService, filesConfigurationService, extensionService, editorService, textResourceConfigurationService, customEditorLabelService); + this.isScratchpad = configurationService.getValue(NotebookSetting.InteractiveWindowPromptToSave) !== true; + } + + override get typeId(): string { + return ReplEditorInput.ID; + } + + override getName() { + return 'REPL'; + } + + override get capabilities() { + const capabilities = super.capabilities; + const scratchPad = this.isScratchpad ? EditorInputCapabilities.Scratchpad : 0; + + return capabilities + | EditorInputCapabilities.Readonly + | scratchPad; + } + + async resolveInput(notebook: NotebookTextModel) { + if (this.inputModelRef) { + return this.inputModelRef.object.textEditorModel; + } + + const lastCell = notebook.cells[notebook.cells.length - 1]; + this.inputModelRef = await this._textModelService.createModelReference(lastCell.uri); + return this.inputModelRef.object.textEditorModel; + } + + override dispose() { + if (!this.isDisposing) { + this.isDisposing = true; + this.editorModelReference?.object.revert({ soft: true }); + this.inputModelRef?.dispose(); + super.dispose(); + } + } +} diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index a4e3fa69d6e..8ec34fb9b6c 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -193,6 +193,9 @@ import 'vs/workbench/contrib/inlineChat/browser/inlineChat.contribution'; // Interactive import 'vs/workbench/contrib/interactive/browser/interactive.contribution'; +// repl +import 'vs/workbench/contrib/replNotebook/browser/repl.contribution'; + // Testing import 'vs/workbench/contrib/testing/browser/testing.contribution';