Skip to content
Merged
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
1 change: 1 addition & 0 deletions news/2 Fixes/7221.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Support saving plotly graphs in the Interactive Window or inside of a notebook.
3 changes: 2 additions & 1 deletion package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -379,5 +379,6 @@
"DataScience.findJupyterCommandProgressCheckInterpreter": "Checking {0}.",
"DataScience.findJupyterCommandProgressSearchCurrentPath": "Searching current path.",
"DataScience.gatheredScriptDescription": "# This file contains only the code required to produce the results of the gathered cell.\n",
"DataScience.gatheredNotebookDescriptionInMarkdown": "# Gathered Notebook\nGenerated from ```{0}```\n\nThis notebook contains only the code and cells required to produce the same results as the gathered cell.\n\nPlease note that the python analysis is quite conservative, so if it is unsure whether a line of code is necessary for execution, it will err on the side of including it."
"DataScience.gatheredNotebookDescriptionInMarkdown": "# Gathered Notebook\nGenerated from ```{0}```\n\nThis notebook contains only the code and cells required to produce the same results as the gathered cell.\n\nPlease note that the python analysis is quite conservative, so if it is unsure whether a line of code is necessary for execution, it will err on the side of including it.",
"DataScience.savePngTitle": "Save Image"
}
1 change: 1 addition & 0 deletions src/client/common/utils/localize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ export namespace DataScience {
export const findJupyterCommandProgressSearchCurrentPath = localize('DataScience.findJupyterCommandProgressSearchCurrentPath', 'Searching current path.');
export const gatheredScriptDescription = localize('DataScience.gatheredScriptDescription', '# This file contains only the code required to produce the results of the gathered cell.\n');
export const gatheredNotebookDescriptionInMarkdown = localize('DataScience.gatheredNotebookDescriptionInMarkdown', '# Gathered Notebook\nGenerated from ```{0}```\n\nThis notebook contains only the code and cells required to produce the same results as the gathered cell.\n\nPlease note that the python analysis is quite conservative, so if it is unsure whether a line of code is necessary for execution, it will err on the side of including it.');
export const savePngTitle = localize('DataScience.savePngTitle', 'Save Image');
}

export namespace DebugConfigStrings {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export enum InteractiveWindowMessages {
LoadTmLanguageResponse = 'load_tmlanguage_response',
OpenLink = 'open_link',
ShowPlot = 'show_plot',
SavePng = 'save_png',
StartDebugging = 'start_debugging',
StopDebugging = 'stop_debugging',
GatherCodeRequest = 'gather_code',
Expand Down Expand Up @@ -332,6 +333,7 @@ export class IInteractiveWindowMapping {
public [InteractiveWindowMessages.LoadTmLanguageResponse]: string | undefined;
public [InteractiveWindowMessages.OpenLink]: string | undefined;
public [InteractiveWindowMessages.ShowPlot]: string | undefined;
public [InteractiveWindowMessages.SavePng]: string | undefined;
public [InteractiveWindowMessages.StartDebugging]: never | undefined;
public [InteractiveWindowMessages.StopDebugging]: never | undefined;
public [InteractiveWindowMessages.GatherCodeRequest]: ICell;
Expand Down
25 changes: 24 additions & 1 deletion src/client/datascience/interactive-common/linkProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { inject, injectable } from 'inversify';
import { Event, EventEmitter } from 'vscode';

import { IApplicationShell } from '../../common/application/types';
import { IFileSystem } from '../../common/platform/types';
import * as localize from '../../common/utils/localize';
import { noop } from '../../common/utils/misc';
import { IInteractiveWindowListener } from '../types';
import { InteractiveWindowMessages } from './interactiveWindowTypes';
Expand All @@ -15,7 +17,10 @@ import { InteractiveWindowMessages } from './interactiveWindowTypes';
@injectable()
export class LinkProvider implements IInteractiveWindowListener {
private postEmitter: EventEmitter<{ message: string; payload: any }> = new EventEmitter<{ message: string; payload: any }>();
constructor(@inject(IApplicationShell) private applicationShell: IApplicationShell) {
constructor(
@inject(IApplicationShell) private applicationShell: IApplicationShell,
@inject(IFileSystem) private fileSystem: IFileSystem
) {
noop();
}

Expand All @@ -30,6 +35,24 @@ export class LinkProvider implements IInteractiveWindowListener {
this.applicationShell.openUrl(payload.toString());
}
break;
case InteractiveWindowMessages.SavePng:
if (payload) {
// Payload should contain the base 64 encoded string. Ask the user to save the file
const filtersObject: Record<string, string[]> = {};
filtersObject[localize.DataScience.pngFilter()] = ['png'];

// Ask the user what file to save to
this.applicationShell.showSaveDialog({
saveLabel: localize.DataScience.savePngTitle(),
filters: filtersObject
}).then(f => {
if (f) {
const buffer = new Buffer(payload.replace('data:image/png;base64', ''), 'base64');
this.fileSystem.writeFile(f.fsPath, buffer).ignoreErrors();
}
});
}
break;
default:
break;
}
Expand Down
7 changes: 5 additions & 2 deletions src/datascience-ui/history-react/interactiveCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,6 @@ export class InteractiveCell extends React.Component<IInteractiveCellProps> {
cellVM={this.props.cellVM}
baseTheme={this.props.baseTheme}
expandImage={this.props.showPlot}
openLink={this.props.openLink}
maxTextSize={this.props.maxTextSize}
themeMatplotlibPlots={themeMatplotlibPlots}
/>
Expand Down Expand Up @@ -236,7 +235,7 @@ export class InteractiveCell extends React.Component<IInteractiveCellProps> {
showWatermark={this.props.showWatermark}
ref={this.codeRef}
monacoTheme={this.props.monacoTheme}
openLink={this.props.openLink}
openLink={this.openLink}
editorMeasureClassName={this.props.editorMeasureClassName}
keyDown={this.isEditCell() ? this.onEditCellKeyDown : undefined}
showLineNumbers={this.props.cellVM.showLineNumbers}
Expand Down Expand Up @@ -346,6 +345,10 @@ export class InteractiveCell extends React.Component<IInteractiveCellProps> {
}
}
}

private openLink = (uri: monacoEditor.Uri) => {
this.props.linkClick(uri.toString());
}
}

// Main export, return a redux connected editor
Expand Down
8 changes: 7 additions & 1 deletion src/datascience-ui/history-react/interactivePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { connect } from 'react-redux';

import { Identifiers } from '../../client/datascience/constants';
import { ContentPanel, IContentPanelProps } from '../interactive-common/contentPanel';
import { handleLinkClick } from '../interactive-common/handlers';
import { ICellViewModel, IMainState } from '../interactive-common/mainState';
import { IStore } from '../interactive-common/redux/store';
import { IVariablePanelProps, VariablePanel } from '../interactive-common/variablePanel';
Expand All @@ -18,7 +19,6 @@ import { getConnectedInteractiveCell } from './interactiveCell';
import { actionCreators } from './redux/actions';

import './interactivePanel.less';

type IInteractivePanelProps = IMainState & typeof actionCreators;

function mapStateToProps(state: IStore): IMainState {
Expand All @@ -38,10 +38,12 @@ export class InteractivePanel extends React.Component<IInteractivePanelProps> {
}

public componentDidMount() {
document.addEventListener('click', this.linkClick, true);
this.props.editorLoaded();
}

public componentWillUnmount() {
document.removeEventListener('click', this.linkClick);
this.props.editorUnmounted();
}

Expand Down Expand Up @@ -259,6 +261,10 @@ export class InteractivePanel extends React.Component<IInteractivePanelProps> {
}
}

private linkClick = (ev: MouseEvent) => {
handleLinkClick(ev, this.props.linkClick);
}

}

// Main export, return a redux connected editor
Expand Down
4 changes: 2 additions & 2 deletions src/datascience-ui/history-react/redux/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
ICodeAction,
ICodeCreatedAction,
IEditCellAction,
IOpenLinkAction,
ILinkClickAction,
IScrollAction,
IShowDataViewerAction,
IShowPlotAction
Expand All @@ -26,7 +26,7 @@ export const actionCreators = {
deleteCell: (cellId: string): CommonAction<ICellAction> => ({ type: CommonActionType.DELETE_CELL, payload: { cellId } }),
undo: (): CommonAction<never | undefined> => ({ type: CommonActionType.UNDO }),
redo: (): CommonAction<never | undefined> => ({ type: CommonActionType.REDO }),
openLink: (uri: monacoEditor.Uri): CommonAction<IOpenLinkAction> => ({ type: CommonActionType.OPEN_LINK, payload: { uri } }),
linkClick: (href: string): CommonAction<ILinkClickAction> => ({ type: CommonActionType.LINK_CLICK, payload: { href } }),
showPlot: (imageHtml: string): CommonAction<IShowPlotAction> => ({ type: CommonActionType.SHOW_PLOT, payload: { imageHtml } }),
toggleInputBlock: (cellId: string): CommonAction<ICellAction> => ({ type: CommonActionType.TOGGLE_INPUT_BLOCK, payload: { cellId } }),
gotoCell: (cellId: string): CommonAction<ICellAction> => ({ type: CommonActionType.GOTO_CELL, payload: { cellId } }),
Expand Down
9 changes: 6 additions & 3 deletions src/datascience-ui/history-react/redux/mapping.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
'use strict';
import { IRefreshVariablesRequest, IScrollToCell } from '../../../client/datascience/interactive-common/interactiveWindowTypes';
import {
IRefreshVariablesRequest,
IScrollToCell
} from '../../../client/datascience/interactive-common/interactiveWindowTypes';
import { IGetCssResponse } from '../../../client/datascience/messages';
import { IGetMonacoThemeResponse } from '../../../client/datascience/monacoMessages';
import { ICell, IJupyterVariable, IJupyterVariablesResponse } from '../../../client/datascience/types';
Expand All @@ -12,7 +15,7 @@ import {
ICellAction,
ICodeAction,
IEditCellAction,
IOpenLinkAction,
ILinkClickAction,
IScrollAction,
IShowDataViewerAction,
IShowPlotAction
Expand All @@ -34,7 +37,7 @@ export class IInteractiveActionMapping {
public [CommonActionType.REDO]: InteractiveReducerFunc<never | undefined>;
public [CommonActionType.SHOW_DATA_VIEWER]: InteractiveReducerFunc<IShowDataViewerAction>;
public [CommonActionType.DELETE_CELL]: InteractiveReducerFunc<ICellAction>;
public [CommonActionType.OPEN_LINK]: InteractiveReducerFunc<IOpenLinkAction>;
public [CommonActionType.LINK_CLICK]: InteractiveReducerFunc<ILinkClickAction>;
public [CommonActionType.SHOW_PLOT]: InteractiveReducerFunc<IShowPlotAction>;
public [CommonActionType.TOGGLE_INPUT_BLOCK]: InteractiveReducerFunc<ICellAction>;
public [CommonActionType.GOTO_CELL]: InteractiveReducerFunc<ICellAction>;
Expand Down
2 changes: 1 addition & 1 deletion src/datascience-ui/history-react/redux/reducers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const reducerMap: IInteractiveActionMapping = {
[CommonActionType.UNDO]: Execution.undo,
[CommonActionType.REDO]: Execution.redo,
[CommonActionType.SHOW_PLOT]: Transfer.showPlot,
[CommonActionType.OPEN_LINK]: Transfer.openLink,
[CommonActionType.LINK_CLICK]: Transfer.linkClick,
[CommonActionType.GOTO_CELL]: Transfer.gotoCell,
[CommonActionType.TOGGLE_INPUT_BLOCK]: Effects.toggleInputBlock,
[CommonActionType.COPY_CELL_CODE]: Transfer.copyCellCode,
Expand Down
23 changes: 2 additions & 21 deletions src/datascience-ui/interactive-common/cellOutput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import { nbformat } from '@jupyterlab/coreutils';
import { JSONObject } from '@phosphor/coreutils';
import ansiRegex from 'ansi-regex';
import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api';
import * as React from 'react';
import '../../client/common/extensions';
import { concatMultilineStringInput, concatMultilineStringOutput } from '../../client/datascience/common';
Expand All @@ -31,7 +30,6 @@ interface ICellOutputProps {
maxTextSize?: number;
hideOutput?: boolean;
themeMatplotlibPlots?: boolean;
openLink(uri: monacoEditor.Uri): void;
expandImage(imageHtml: string): void;
}

Expand Down Expand Up @@ -328,23 +326,6 @@ export class CellOutput extends React.Component<ICellOutputProps> {
}
}

private click = (event: React.MouseEvent<HTMLDivElement>) => {
// If this is an anchor element, forward the click as Jupyter does.
let anchor = event.target as HTMLAnchorElement;
if (anchor && anchor.href) {
// Href may be redirected to an inner anchor
if (anchor.href.startsWith('vscode')) {
const inner = anchor.getElementsByTagName('a');
if (inner && inner.length > 0) {
anchor = inner[0];
}
}
if (anchor && anchor.href && !anchor.href.startsWith('vscode')) {
this.props.openLink(monacoEditor.Uri.parse(anchor.href));
}
}
}

// tslint:disable-next-line: max-func-body-length
private renderOutputs(outputs: nbformat.IOutput[]): JSX.Element[] {
return [this.renderOutput(outputs)];
Expand All @@ -368,7 +349,7 @@ export class CellOutput extends React.Component<ICellOutputProps> {
// If we are not theming plots then wrap them in a white span
if (transformed.outputSpanClassName) {
buffer.push(
<div role='group' key={index} onDoubleClick={transformed.doubleClick} onClick={this.click} className={className}>
<div role='group' key={index} onDoubleClick={transformed.doubleClick} className={className}>
<span className={transformed.outputSpanClassName}>
{transformed.extraButton}
<Transform data={transformed.data} />
Expand All @@ -377,7 +358,7 @@ export class CellOutput extends React.Component<ICellOutputProps> {
);
} else {
buffer.push(
<div role='group' key={index} onDoubleClick={transformed.doubleClick} onClick={this.click} className={className}>
<div role='group' key={index} onDoubleClick={transformed.doubleClick} className={className}>
{transformed.extraButton}
<Transform data={transformed.data} />
</div>
Expand Down
20 changes: 20 additions & 0 deletions src/datascience-ui/interactive-common/handlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
'use strict';

export function handleLinkClick(ev: MouseEvent, linkClick: (href: string) => void) {
// If this is an anchor element, forward the click as Jupyter does.
let anchor = ev.target as HTMLAnchorElement;
if (anchor && anchor.href) {
// Href may be redirected to an inner anchor
if (anchor.href.startsWith('vscode')) {
const inner = anchor.getElementsByTagName('a');
if (inner && inner.length > 0) {
anchor = inner[0];
}
}
if (anchor && anchor.href && !anchor.href.startsWith('vscode')) {
linkClick(anchor.href);
}
}
}
10 changes: 7 additions & 3 deletions src/datascience-ui/interactive-common/redux/reducers/transfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
CommonReducerArg,
ICellAction,
IEditCellAction,
IOpenLinkAction,
ILinkClickAction,
ISendCommandAction,
IShowDataViewerAction,
IShowPlotAction
Expand Down Expand Up @@ -46,8 +46,12 @@ export namespace Transfer {
return arg.prevState;
}

export function openLink<T>(arg: CommonReducerArg<T, IOpenLinkAction>): IMainState {
arg.queueAction(createPostableAction(InteractiveWindowMessages.OpenLink, arg.payload.uri.toString()));
export function linkClick<T>(arg: CommonReducerArg<T, ILinkClickAction>): IMainState {
if (arg.payload.href.startsWith('data:image/png')) {
arg.queueAction(createPostableAction(InteractiveWindowMessages.SavePng, arg.payload.href));
} else {
arg.queueAction(createPostableAction(InteractiveWindowMessages.OpenLink, arg.payload.href));
}
return arg.prevState;
}

Expand Down
6 changes: 3 additions & 3 deletions src/datascience-ui/interactive-common/redux/reducers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ export enum CommonActionType {
INSERT_BELOW = 'action.insert_below',
INTERRUPT_KERNEL = 'action.interrupt_kernel_action',
LOADED_ALL_CELLS = 'action.loaded_all_cells',
LINK_CLICK = 'action.link_click',
MOVE_CELL_DOWN = 'action.move_cell_down',
MOVE_CELL_UP = 'action.move_cell_up',
OPEN_LINK = 'action.open_link',
REDO = 'action.redo',
REFRESH_VARIABLES = 'action.refresh_variables',
RESTART_KERNEL = 'action.restart_kernel_action',
Expand All @@ -77,8 +77,8 @@ export enum CommonActionType {
export interface IShowDataViewerAction extends IShowDataViewer {
}

export interface IOpenLinkAction {
uri: monacoEditor.Uri;
export interface ILinkClickAction {
href: string;
}

export interface IShowPlotAction {
Expand Down
8 changes: 6 additions & 2 deletions src/datascience-ui/native-editor/nativeCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,7 @@ export class NativeCell extends React.Component<INativeCellProps> {
showWatermark={false}
ref={this.inputRef}
monacoTheme={this.props.monacoTheme}
openLink={this.props.openLink}
openLink={this.openLink}
editorMeasureClassName={undefined}
focused={this.onCodeFocused}
unfocused={this.onCodeUnfocused}
Expand Down Expand Up @@ -601,7 +601,6 @@ export class NativeCell extends React.Component<INativeCellProps> {
cellVM={this.props.cellVM}
baseTheme={this.props.baseTheme}
expandImage={this.props.showPlot}
openLink={this.props.openLink}
maxTextSize={this.props.maxTextSize}
themeMatplotlibPlots={themeMatplotlibPlots}
/>
Expand Down Expand Up @@ -653,6 +652,11 @@ export class NativeCell extends React.Component<INativeCellProps> {

return <div className={classes}></div>;
}

private openLink = (uri: monacoEditor.Uri) => {
this.props.linkClick(uri.toString());
}

}

// Main export, return a redux connected editor
Expand Down
8 changes: 8 additions & 0 deletions src/datascience-ui/native-editor/nativeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { OSType } from '../../client/common/utils/platform';
import { concatMultilineStringInput } from '../../client/datascience/common';
import { NativeCommandType } from '../../client/datascience/interactive-common/interactiveWindowTypes';
import { ContentPanel, IContentPanelProps } from '../interactive-common/contentPanel';
import { handleLinkClick } from '../interactive-common/handlers';
import { ICellViewModel, IMainState } from '../interactive-common/mainState';
import { IStore } from '../interactive-common/redux/store';
import { IVariablePanelProps, VariablePanel } from '../interactive-common/variablePanel';
Expand Down Expand Up @@ -43,11 +44,13 @@ export class NativeEditor extends React.Component<INativeEditorProps> {
this.props.editorLoaded();
window.addEventListener('keydown', this.mainKeyDown);
window.addEventListener('resize', () => this.forceUpdate(), true);
document.addEventListener('click', this.linkClick, true);
}

public componentWillUnmount() {
window.removeEventListener('keydown', this.mainKeyDown);
window.removeEventListener('resize', () => this.forceUpdate());
document.removeEventListener('click', this.linkClick);
this.props.editorUnmounted();
}

Expand Down Expand Up @@ -336,6 +339,11 @@ export class NativeEditor extends React.Component<INativeEditorProps> {
private scrollDiv = (_div: HTMLDivElement) => {
// Doing nothing for now. This should be implemented once redux refactor is done.
}

private linkClick = (ev: MouseEvent) => {
handleLinkClick(ev, this.props.linkClick);
}

}

// Main export, return a redux connected editor
Expand Down
Loading