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 @@ -56,7 +56,7 @@ import { POSITRON_EXECUTE_CELL_COMMAND_ID, POSITRON_NOTEBOOK_EDITOR_ID, POSITRON
import { getActiveCell, getSelectedCells, SelectionState } from './selectionMachine.js';
import { POSITRON_NOTEBOOK_CELL_CONTEXT_KEYS as CELL_CONTEXT_KEYS, POSITRON_NOTEBOOK_CELL_EDITOR_FOCUSED, POSITRON_NOTEBOOK_EDITOR_FOCUSED, POSITRON_NOTEBOOK_CELL_HAS_OUTPUTS, POSITRON_NOTEBOOK_CELL_JSON_OUTPUT_COUNT, POSITRON_NOTEBOOK_CELL_IMAGE_OUTPUT_COUNT, POSITRON_NOTEBOOK_CELL_OUTPUT_COLLAPSED, POSITRON_NOTEBOOK_CELL_OUTPUT_OVERFLOWS, POSITRON_NOTEBOOK_CELL_OUTPUT_SCROLLING, POSITRON_NOTEBOOK_EXPERIMENTAL, POSITRON_NOTEBOOK_OUTPUT_IMAGE_TARGETED, POSITRON_NOTEBOOK_OUTPUT_JSON_TARGETED } from './ContextKeysManager.js';
import './contrib/undoRedo/positronNotebookUndoRedo.js';
import { registerAction2, MenuId, MenuRegistry } from '../../../../platform/actions/common/actions.js';
import { Action2, registerAction2, MenuId, MenuRegistry } from '../../../../platform/actions/common/actions.js';
import { ExecuteSelectionInConsoleAction } from './ExecuteSelectionInConsoleAction.js';
import { ThemeIcon } from '../../../../base/common/themables.js';
import { KernelStatusBadge } from './KernelStatusBadge.js';
Expand All @@ -76,6 +76,8 @@ import './AskAssistantAction.js'; // Register AskAssistantAction
import { CONTEXT_FIND_INPUT_FOCUSED, CONTEXT_REPLACE_INPUT_FOCUSED } from '../../../../editor/contrib/find/browser/findModel.js';
import { Codicon } from '../../../../base/common/codicons.js';
import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js';
import { IViewsService } from '../../../services/views/common/viewsService.js';
import { IOutlinePane } from '../../outline/browser/outline.js';

export const POSITRON_NOTEBOOK_COMMAND_MODE = ContextKeyExpr.and(
POSITRON_NOTEBOOK_EDITOR_FOCUSED,
Expand Down Expand Up @@ -2103,6 +2105,39 @@ registerAction2(class extends NotebookAction2 {
}
});

// Toggle Outline - Reveals or hides the Outline view in the Explorer pane
export class ToggleOutlineAction extends Action2 {
constructor() {
super({
id: 'positronNotebook.toggleOutline',
title: localize2('toggleOutline', 'Toggle Outline'),
icon: ThemeIcon.fromId('list-unordered'),
f1: true,
category: POSITRON_NOTEBOOK_CATEGORY,
positronActionBarOptions: {
controlType: 'button',
displayTitle: false
},
menu: {
id: MenuId.EditorActionsLeft,
group: 'navigation',
order: 45,
when: ContextKeyExpr.equals('activeEditor', POSITRON_NOTEBOOK_EDITOR_ID)
}
});
}

override async run(accessor: ServicesAccessor): Promise<void> {
const viewsService = accessor.get(IViewsService);
if (viewsService.isViewVisible(IOutlinePane.Id)) {
viewsService.closeView(IOutlinePane.Id);
} else {
await viewsService.openView(IOutlinePane.Id, true);
}
}
}
registerAction2(ToggleOutlineAction);

// Ask Assistant - Opens assistant chat with prompt options for the notebook
// Action is defined in AskAssistantAction.ts

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*---------------------------------------------------------------------------------------------
* Copyright (C) 2026 Posit Software, PBC. All rights reserved.
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/

/// <reference types="vitest/globals" />

import { createTestContainer } from '../../../../../test/vitest/positronTestContainer.js';
import { stubInterface } from '../../../../../test/vitest/stubInterface.js';
import { ToggleOutlineAction } from '../../browser/positronNotebook.contribution.js';
import { IOutlinePane } from '../../../outline/browser/outline.js';
import { IViewsService } from '../../../../services/views/common/viewsService.js';

describe('ToggleOutlineAction', () => {
const ctx = createTestContainer()
.withWorkbenchServices()
.build();

let isViewVisible: ReturnType<typeof vi.fn<IViewsService['isViewVisible']>>;
// openView is generic (<T extends IView>); store it with a wider mock signature and
// re-narrow inside the stub via a closure so stubInterface's Partial<IViewsService>
// stays type-correct.
let openView: ReturnType<typeof vi.fn<(id: string, focus?: boolean) => Promise<unknown>>>;
let closeView: ReturnType<typeof vi.fn<IViewsService['closeView']>>;

beforeEach(() => {
isViewVisible = vi.fn();
openView = vi.fn<(id: string, focus?: boolean) => Promise<unknown>>().mockResolvedValue(null);
closeView = vi.fn();
ctx.instantiationService.stub(IViewsService, stubInterface<IViewsService>({
isViewVisible,
openView: <T>(id: string, focus?: boolean) => openView(id, focus) as Promise<T | null>,
closeView,
}));
});

async function run(): Promise<void> {
const action = new ToggleOutlineAction();
await ctx.instantiationService.invokeFunction(accessor => action.run(accessor));
}

it('opens and focuses the outline view when it is not visible', async () => {
isViewVisible.mockReturnValue(false);

await run();

expect(openView).toHaveBeenCalledWith(IOutlinePane.Id, true);
expect(closeView).not.toHaveBeenCalled();
});

it('closes the outline view when it is visible', async () => {
isViewVisible.mockReturnValue(true);

await run();

expect(closeView).toHaveBeenCalledWith(IOutlinePane.Id);
expect(openView).not.toHaveBeenCalled();
});
});
Loading