Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { NotebookTextDiffEditor } from '../../notebook/browser/diff/notebookDiff
import { NotebookMultiTextDiffEditor } from '../../notebook/browser/diff/notebookMultiDiffEditor.js';
// --- Start Positron ---
// Imports to support inline chat in Positron notebooks.
import { IPositronNotebookService } from '../../positronNotebook/browser/positronNotebookService.js';
import { getAllPositronNotebookInstances } from '../../positronNotebook/browser/notebookUtils.js';
// --- End Positron ---

export class InlineChatNotebookContribution {
Expand All @@ -28,10 +28,6 @@ export class InlineChatNotebookContribution {
@IInlineChatSessionService sessionService: IInlineChatSessionService,
@IEditorService editorService: IEditorService,
@INotebookEditorService notebookEditorService: INotebookEditorService,
// --- Start Positron ---
// Imports to support inline chat in Positron notebooks.
@IPositronNotebookService positronNotebookService: IPositronNotebookService,
// --- End Positron ---
) {

this._store.add(sessionService.registerSessionKeyComputer(Schemas.vscodeNotebookCell, {
Expand Down Expand Up @@ -68,7 +64,7 @@ export class InlineChatNotebookContribution {
// --- Start Positron ---
// To support inline chat in Positron notebooks:
// construct a session comparison key from the corresponding notebook
for (const positronInstance of positronNotebookService.listInstances(data.notebook)) {
for (const positronInstance of getAllPositronNotebookInstances(editorService, data.notebook)) {
const candidate = `<positron-notebook>${positronInstance.id}#${uri}`;
if (!fallback) {
fallback = candidate;
Expand Down Expand Up @@ -120,7 +116,7 @@ export class InlineChatNotebookContribution {
// --- Start Positron ---
// To support inline chat in Positron notebooks:
// cancel existing chat sessions when a new one is started.
for (const positronInstance of positronNotebookService.listInstances(candidate.notebook)) {
for (const positronInstance of getAllPositronNotebookInstances(editorService, candidate.notebook)) {
if (positronInstance.hasCodeEditor(newSessionEditor)) {
for (const { editor } of positronInstance.cells.get()) {
if (editor && editor !== newSessionEditor) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ import { getFormattedNotebookMetadataJSON } from '../common/model/notebookMetada
import { NotebookOutputEditor } from './outputEditor/notebookOutputEditor.js';
import { NotebookOutputEditorInput } from './outputEditor/notebookOutputEditorInput.js';
// --- Start Positron ---
import { IPositronNotebookService } from '../../positronNotebook/browser/positronNotebookService.js';
import { hasConnectedNotebookForUri } from '../../positronNotebook/browser/notebookUtils.js';
// --- End Positron ---

/*--------------------------------------------------------------------------------------------- */
Expand Down Expand Up @@ -774,9 +774,6 @@ class NotebookEditorManager implements IWorkbenchContribution {
constructor(
@IEditorService private readonly _editorService: IEditorService,
@INotebookEditorModelResolverService private readonly _notebookEditorModelService: INotebookEditorModelResolverService,
// --- Start Positron ---
@IPositronNotebookService private readonly _positronNotebookService: IPositronNotebookService,
// --- End Positron ---
@IEditorGroupsService editorGroups: IEditorGroupsService
) {
// OPEN notebook editor for models that have turned dirty without being visible in an editor
Expand Down Expand Up @@ -808,8 +805,7 @@ class NotebookEditorManager implements IWorkbenchContribution {
// --- Start Positron ---
// Make sure that we dont try and open the same editor twice if we're using positron
// notebooks.
const positronInstances = this._positronNotebookService.listInstances(model.resource);
if (positronInstances.some(instance => instance.connectedToEditor)) {
if (hasConnectedNotebookForUri(this._editorService, model.resource)) {
continue;
}
// --- End Positron ---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,11 @@ export interface IPositronNotebookCell extends Disposable {
*/
attachContainer(container: HTMLElement): void;

/**
* Get the container that the cell is attached to
*/
get container(): HTMLElement | undefined;

/**
*
* @param editor Code editor widget associated with cell.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,9 @@ export abstract class PositronNotebookCellGeneral extends Disposable implements
this._container = container;
}

get container(): HTMLElement | undefined {
return this._container;
}

attachEditor(editor: CodeEditorWidget): void {
this._editor.set(editor, undefined);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,13 +277,20 @@ export class PositronNotebookEditor extends AbstractEditorWithViewState<INoteboo
notebookInstance.attachView(this._parentDiv, scopedContextKeyService);
}

override clearInput(): void {
this._logService.info(this._identifier, 'clearInput');
/**
* Called when this composite should receive keyboard focus.
*/
override focus(): void {
super.focus();

if (this.notebookInstance && this._parentDiv) {
this.notebookInstance.detachView();
console.log('isVisible', this._isVisible.get());
// Drive focus into the notebook instance based on selection state
if (this.notebookInstance) {
this.notebookInstance.grabFocus();
}
}

override clearInput(): void {
this._logService.info(this._identifier, 'clearInput');

if (this.notebookInstance) {
this.notebookInstance.detachView();
Expand Down Expand Up @@ -353,7 +360,11 @@ export class PositronNotebookEditor extends AbstractEditorWithViewState<INoteboo
// Create a scoped context key service rooted at the notebook container so cell scopes inherit it.
const scopedContextKeyService = this._containerScopedContextKeyService = this.contextKeyService.createScoped(this._parentDiv);

const reactRenderer: PositronReactRenderer = this._positronReactRenderer ?? new PositronReactRenderer(this._parentDiv);
// Create renderer if it doesn't exist, otherwise reuse existing renderer
if (!this._positronReactRenderer) {
this._positronReactRenderer = new PositronReactRenderer(this._parentDiv);
}
const reactRenderer = this._positronReactRenderer;

reactRenderer.render(
<NotebookVisibilityProvider isVisible={this._isVisible}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -979,6 +979,41 @@ export class PositronNotebookInstance extends Disposable implements IPositronNot
this._positronConsoleService.showNotebookConsole(this.uri, true);
}

/**
* Grabs focus for this notebook based on the current selection state.
* Called when the notebook editor receives focus from the workbench.
*
* Note: This method may be called twice during tab switches:
* - First call: Early, cells may not be rendered yet (no-op via optional chaining)
* - Second call: After render completes, focus succeeds
*/
grabFocus(): void {
const state = this.selectionStateMachine.state.get();

switch (state.type) {
case SelectionState.EditingSelection:
// Focus the editor - enterEditor() already has idempotency checks
this.selectionStateMachine.enterEditor(state.selected);
break;

case SelectionState.SingleSelection:
case SelectionState.MultiSelection: {
// Focus the first selected cell's container
// Optional chaining handles undefined containers gracefully
const cell = state.type === SelectionState.SingleSelection
? state.selected
: state.selected[0];
cell.container?.focus();
break;
}

case SelectionState.NoCells:
// Fall back to notebook container
this._container?.focus();
break;
}
}

/**
* Clears the outputs of all cells in the notebook.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ import { IQuickInputService, IQuickPickItem } from '../../../../platform/quickin
import { selectKernelIcon } from '../../notebook/browser/notebookIcons.js';
import { INotebookKernelService, INotebookKernel } from '../../notebook/common/notebookKernelService.js';
import { PositronNotebookInstance } from './PositronNotebookInstance.js';
import { IPositronNotebookService } from './positronNotebookService.js';
import { POSITRON_RUNTIME_NOTEBOOK_KERNELS_EXTENSION_ID } from '../../runtimeNotebookKernel/common/runtimeNotebookKernelConfig.js';
import { IEditorService } from '../../../services/editor/common/editorService.js';
import { getActiveNotebook } from './notebookUtils.js';

export const SELECT_KERNEL_ID_POSITRON = 'positronNotebook.selectKernel';
const NOTEBOOK_ACTIONS_CATEGORY_POSITRON = localize2('positronNotebookActions.category', 'Positron Notebook');
Expand All @@ -38,8 +39,7 @@ class SelectPositronNotebookKernelAction extends Action2 {
async run(accessor: ServicesAccessor, context?: SelectPositronNotebookKernelContext): Promise<boolean> {
const { forceDropdown } = context || { forceDropdown: false };
const notebookKernelService = accessor.get(INotebookKernelService);
const notebookService = accessor.get(IPositronNotebookService);
const activeNotebook = notebookService.getActiveInstance();
const activeNotebook = getActiveNotebook(accessor.get(IEditorService));
const quickInputService = accessor.get(IQuickInputService);

if (!activeNotebook) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,19 @@
import { Disposable } from '../../../../../../base/common/lifecycle.js';
import { WorkbenchPhase, registerWorkbenchContribution2 } from '../../../../../common/contributions.js';
import { UndoCommand, RedoCommand } from '../../../../../../editor/browser/editorExtensions.js';
import { IPositronNotebookService } from '../../positronNotebookService.js';
import { POSITRON_NOTEBOOK_EDITOR_CONTAINER_FOCUSED, POSITRON_NOTEBOOK_CELL_EDITOR_FOCUSED } from '../../ContextKeysManager.js';
import { IUndoRedoService } from '../../../../../../platform/undoRedo/common/undoRedo.js';
import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js';
import { IEditorService } from '../../../../../services/editor/common/editorService.js';
import { getActiveNotebook } from '../../notebookUtils.js';

class PositronNotebookUndoRedoContribution extends Disposable {

static readonly ID = 'workbench.contrib.positronNotebookUndoRedo';

constructor(
@IUndoRedoService private readonly undoRedoService: IUndoRedoService,
@IPositronNotebookService private readonly positronNotebookService: IPositronNotebookService,
@IEditorService private readonly editorService: IEditorService,
@IContextKeyService private readonly contextKeyService: IContextKeyService
) {
super();
Expand All @@ -30,7 +31,7 @@ class PositronNotebookUndoRedoContribution extends Disposable {

private shouldHandleUndoRedo(): boolean {
// Get the active notebook instance to access its scoped context key service
const instance = this.positronNotebookService.getActiveInstance();
const instance = getActiveNotebook(this.editorService);
if (!instance) {
return false;
}
Expand Down Expand Up @@ -61,7 +62,7 @@ class PositronNotebookUndoRedoContribution extends Disposable {
return false;
}

const instance = this.positronNotebookService.getActiveInstance();
const instance = getActiveNotebook(this.editorService);
if (!instance) {
return false;
}
Expand All @@ -79,7 +80,7 @@ class PositronNotebookUndoRedoContribution extends Disposable {
return false;
}

const instance = this.positronNotebookService.getActiveInstance();
const instance = getActiveNotebook(this.editorService);
if (!instance) {
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

import { CommandsRegistry, ICommandMetadata } from '../../../../../../platform/commands/common/commands.js';
import { ServicesAccessor } from '../../../../../../platform/instantiation/common/instantiation.js';
import { IPositronNotebookService } from '../../positronNotebookService.js';
import { IPositronNotebookCell } from '../../PositronNotebookCells/IPositronNotebookCell.js';
import { NotebookCellActionBarRegistry, INotebookCellActionBarItem } from './actionBarRegistry.js';
import { IDisposable, DisposableStore } from '../../../../../../base/common/lifecycle.js';
Expand All @@ -15,6 +14,8 @@ import { IPositronNotebookCommandKeybinding } from './commandUtils.js';
import { IPositronNotebookInstance } from '../../IPositronNotebookInstance.js';
import { getSelectedCell, getSelectedCells, getEditingCell } from '../../selectionMachine.js';
import { ContextKeyExpr, ContextKeyExpression } from '../../../../../../platform/contextkey/common/contextkey.js';
import { IEditorService } from '../../../../../services/editor/common/editorService.js';
import { getActiveNotebook } from '../../notebookUtils.js';

/**
* Options for registering a cell command.
Expand Down Expand Up @@ -80,8 +81,8 @@ export function registerCellCommand({
const commandDisposable = CommandsRegistry.registerCommand({
id: commandId,
handler: (accessor: ServicesAccessor) => {
const notebookService = accessor.get(IPositronNotebookService);
const activeNotebook = notebookService.getActiveInstance();
const editorService = accessor.get(IEditorService);
const activeNotebook = getActiveNotebook(editorService);
if (!activeNotebook) {
return;
}
Expand Down
83 changes: 83 additions & 0 deletions src/vs/workbench/contrib/positronNotebook/browser/notebookUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*---------------------------------------------------------------------------------------------
* Copyright (C) 2025 Posit Software, PBC. All rights reserved.
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/

import { URI } from '../../../../base/common/uri.js';
import { isEqual } from '../../../../base/common/resources.js';
import { IEditorService } from '../../../services/editor/common/editorService.js';
import { IPositronNotebookInstance } from './IPositronNotebookInstance.js';
import { PositronNotebookEditor } from './PositronNotebookEditor.js';
import { POSITRON_NOTEBOOK_EDITOR_ID } from '../common/positronNotebookCommon.js';

/**
* Retrieves the active Positron notebook instance from the editor service.
*
* @param editorService The editor service
* @returns The active notebook instance, or undefined if no Positron notebook is active
*/
export function getActiveNotebook(editorService: IEditorService): IPositronNotebookInstance | undefined {
const activeEditorPane = editorService.activeEditorPane;

// Check if the active editor is a Positron Notebook Editor
if (!activeEditorPane || activeEditorPane.getId() !== POSITRON_NOTEBOOK_EDITOR_ID) {
return undefined;
}

// Extract the notebook instance from the editor
const activeNotebook = (activeEditorPane as PositronNotebookEditor).notebookInstance;
return activeNotebook;
}

/**
* Checks if any notebook instance for a given URI is connected to an editor.
*
* @param editorService The editor service
* @param uri The notebook URI to check
* @returns True if any notebook instance for this URI is connected to an editor, false otherwise
*/
export function hasConnectedNotebookForUri(
editorService: IEditorService,
uri: URI
): boolean {
for (const editorPane of editorService.visibleEditorPanes) {
if (editorPane.getId() === POSITRON_NOTEBOOK_EDITOR_ID) {
const notebookEditor = editorPane as PositronNotebookEditor;
const instance = notebookEditor.notebookInstance;

if (instance && isEqual(instance.uri, uri) && instance.connectedToEditor) {
return true;
}
}
}
return false;
}

/**
* Retrieves all Positron notebook instances from visible editor panes.
*
* @param editorService The editor service
* @param uri Optional URI to filter instances by
* @returns Array of all notebook instances, optionally filtered by URI
*/
export function getAllPositronNotebookInstances(
editorService: IEditorService,
uri?: URI
): IPositronNotebookInstance[] {
const instances: IPositronNotebookInstance[] = [];

for (const editorPane of editorService.visibleEditorPanes) {
if (editorPane.getId() === POSITRON_NOTEBOOK_EDITOR_ID) {
const notebookEditor = editorPane as PositronNotebookEditor;
const instance = notebookEditor.notebookInstance;

if (instance) {
if (!uri || isEqual(instance.uri, uri)) {
instances.push(instance);
}
}
}
}

return instances;
}
Loading