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
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#
# Copyright (C) 2023-2024 Posit Software, PBC. All rights reserved.
# Copyright (C) 2023-2025 Posit Software, PBC. All rights reserved.
# Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
#

Expand All @@ -17,7 +17,7 @@

from positron.positron_ipkernel import PositronIPyKernel, PositronShell
from positron.ui import UiService
from positron.ui_comm import UiFrontendEvent
from positron.ui_comm import ShowHtmlFileDestination, UiFrontendEvent
from positron.utils import alias_home

from .conftest import DummyComm
Expand Down Expand Up @@ -66,9 +66,9 @@ def show_url_event(url: str) -> Dict[str, Any]:
return json_rpc_notification(UiFrontendEvent.ShowUrl, {"url": url, "source": None})


def show_html_file_event(path: str, *, is_plot: bool) -> Dict[str, Any]:
def show_html_file_event(path: str, *, destination: str) -> Dict[str, Any]:
return json_rpc_notification(
"show_html_file", {"path": path, "is_plot": is_plot, "height": 0, "title": ""}
"show_html_file", {"path": path, "destination": destination, "height": 0, "title": ""}
)


Expand Down Expand Up @@ -178,12 +178,21 @@ def test_shutdown(ui_service: UiService, ui_comm: DummyComm) -> None:
# Unix path
(
"file://hello/my/friend.html",
[show_html_file_event("file://hello/my/friend.html", is_plot=False)],
[
show_html_file_event(
"file://hello/my/friend.html", destination=ShowHtmlFileDestination.Viewer
)
],
),
# Windows path
(
"file:///C:/Users/username/Documents/index.htm",
[show_html_file_event("file:///C:/Users/username/Documents/index.htm", is_plot=False)],
[
show_html_file_event(
"file:///C:/Users/username/Documents/index.htm",
destination=ShowHtmlFileDestination.Viewer,
)
],
),
# Not a local html file
("http://example.com/page.html", []),
Expand Down Expand Up @@ -233,7 +242,7 @@ def test_bokeh_show_sends_events(
assert len(ui_comm.messages) == 1
params = ui_comm.messages[0]["data"]["params"]
assert params["title"] == ""
assert params["is_plot"]
assert params["destination"] == "plot"
assert params["height"] == 0
# default behavior should be writing to temppath
# not wherever the process is running (see patch.bokeh)
Expand Down Expand Up @@ -281,11 +290,11 @@ def test_plotly_show_sends_events(
assert len(ui_comm.messages) == 2
params = ui_comm.messages[0]["data"]["params"]
assert params["title"] == ""
assert params["is_plot"]
assert params["destination"] == "plot"
assert params["height"] == 0
params = ui_comm.messages[1]["data"]["params"]
assert params["title"] == ""
assert params["is_plot"]
assert params["destination"] == "plot"
assert params["height"] == 0


Expand All @@ -296,7 +305,7 @@ def test_is_not_plot_url_events(
"""
Test that opening a URL that is not a plot sends the expected UI events.

Checks that the `is_plot` parameter is not sent or is `False`.
Checks that the `destination` parameter is not set to "plot".
"""
shell.run_cell(
"""\
Expand All @@ -311,8 +320,8 @@ def test_is_not_plot_url_events(
assert len(ui_comm.messages) == 2
params = ui_comm.messages[0]["data"]["params"]
assert params["url"] == "http://127.0.0.1:8000"
assert "is_plot" not in params
assert "destination" not in params

params = ui_comm.messages[1]["data"]["params"]
assert params["path"] == "file.html" if sys.platform == "win32" else "file://file.html"
assert params["is_plot"] is False
assert params["destination"] != "plot"
17 changes: 11 additions & 6 deletions extensions/positron-python/python_files/posit/positron/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
CallMethodParams,
CallMethodRequest,
OpenEditorParams,
ShowHtmlFileDestination,
ShowHtmlFileParams,
ShowUrlParams,
UiBackendMessageContent,
Expand Down Expand Up @@ -232,21 +233,25 @@ def open(self, url, new=0, autoraise=True) -> bool: # noqa: ARG002, FBT002
if not self._comm:
return False

is_plot = False
destination = ShowHtmlFileDestination.Viewer
# If url is pointing to an HTML file, route to the ShowHtmlFile comm
if is_local_html_file(url):
# Send bokeh plots to the plots pane.
# Identify bokeh plots by checking the stack for the bokeh.io.showing.show function.
# This is not great but currently the only information we have.
is_plot = self._is_module_function("bokeh.io.showing", "show")
destination = (
ShowHtmlFileDestination.Plot
if self._is_module_function("bokeh.io.showing", "show")
else ShowHtmlFileDestination.Viewer
)

return self._send_show_html_event(url, is_plot)
return self._send_show_html_event(url, destination)

for addr in _localhosts:
if addr in url:
is_plot = self._is_module_function("plotly.basedatatypes")
if is_plot:
return self._send_show_html_event(url, is_plot)
return self._send_show_html_event(url, ShowHtmlFileDestination.Plot)
else:
event = ShowUrlParams(url=url)
self._comm.send_event(name=UiFrontendEvent.ShowUrl, payload=event.dict())
Expand All @@ -271,7 +276,7 @@ def _is_module_function(module_name: str, function_name: Union[str, None] = None
return True
return False

def _send_show_html_event(self, url: str, is_plot: bool) -> bool: # noqa: FBT001
def _send_show_html_event(self, url: str, destination: str) -> bool:
if self._comm is None:
logger.warning("No comm available to send ShowHtmlFile event")
return False
Expand All @@ -283,7 +288,7 @@ def _send_show_html_event(self, url: str, is_plot: bool) -> bool: # noqa: FBT00
path=url,
# Use the URL's title.
title="",
is_plot=is_plot,
destination=destination,
# No particular height is required.
height=0,
).dict(),
Expand Down
17 changes: 15 additions & 2 deletions extensions/positron-python/python_files/posit/positron/ui_comm.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@ class OpenEditorKind(str, enum.Enum):
Uri = "uri"


@enum.unique
class ShowHtmlFileDestination(str, enum.Enum):
"""
Possible values for Destination in ShowHtmlFile
"""

Plot = "plot"

Viewer = "viewer"

Editor = "editor"


@enum.unique
class PreviewSourceType(str, enum.Enum):
"""
Expand Down Expand Up @@ -569,8 +582,8 @@ class ShowHtmlFileParams(BaseModel):
description="A title to be displayed in the viewer. May be empty, and can be superseded by the title in the HTML file.",
)

is_plot: StrictBool = Field(
description="Whether the HTML file is a plot-like object",
destination: ShowHtmlFileDestination = Field(
description="Where the file should be shown in Positron: as an interactive plot, in the viewer pane, or in a new editor tab.",
)

height: StrictInt = Field(
Expand Down
2 changes: 1 addition & 1 deletion extensions/positron-r/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -955,7 +955,7 @@
},
"positron": {
"binaryDependencies": {
"ark": "0.1.220"
"ark": "0.1.221"
},
"minimumRVersion": "4.2.0",
"minimumRenvVersion": "1.0.9"
Expand Down
11 changes: 8 additions & 3 deletions positron/comms/ui-frontend-openrpc.json
Original file line number Diff line number Diff line change
Expand Up @@ -514,10 +514,15 @@
}
},
{
"name": "is_plot",
"description": "Whether the HTML file is a plot-like object",
"name": "destination",
"description": "Where the file should be shown in Positron: as an interactive plot, in the viewer pane, or in a new editor tab.",
"schema": {
"type": "boolean"
"type": "string",
"enum": [
"plot",
"viewer",
"editor"
]
}
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { decodeBase64 } from '../../../../base/common/buffer.js';
import { SavePlotOptions, showSavePlotModalDialog } from './modalDialogs/savePlotModalDialog.js';
import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';
import { localize } from '../../../../nls.js';
import { UiFrontendEvent } from '../../../services/languageRuntime/common/positronUiComm.js';
import { ShowHtmlFileDestination, UiFrontendEvent } from '../../../services/languageRuntime/common/positronUiComm.js';
import { IShowHtmlUriEvent } from '../../../services/languageRuntime/common/languageRuntimeUiClient.js';
import { IPositronPreviewService } from '../../positronPreview/browser/positronPreviewSevice.js';
import { NotebookOutputPlotClient } from './notebookOutputPlotClient.js';
Expand Down Expand Up @@ -241,7 +241,7 @@ export class PositronPlotsService extends Disposable implements IPositronPlotsSe
// If we have a new HTML file to show, turn it into a webview plot.
if (event.event.name === UiFrontendEvent.ShowHtmlFile) {
const data = event.event.data as IShowHtmlUriEvent;
if (data.event.is_plot) {
if (data.event.destination === ShowHtmlFileDestination.Plot) {
await this.createWebviewPlot(event.session_id, data);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*---------------------------------------------------------------------------------------------
* Copyright (C) 2023-2024 Posit Software, PBC. All rights reserved.
* Copyright (C) 2023-2025 Posit Software, PBC. All rights reserved.
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/

Expand All @@ -16,7 +16,7 @@ import { ILanguageRuntimeSession, IRuntimeSessionService } from '../../../servic
import { IPositronNotebookOutputWebviewService } from '../../positronOutputWebview/browser/notebookOutputWebviewService.js';
import { URI } from '../../../../base/common/uri.js';
import { PreviewUrl } from './previewUrl.js';
import { ShowHtmlFileEvent, ShowUrlEvent, UiFrontendEvent } from '../../../services/languageRuntime/common/positronUiComm.js';
import { ShowHtmlFileDestination, ShowHtmlFileEvent, ShowUrlEvent, UiFrontendEvent } from '../../../services/languageRuntime/common/positronUiComm.js';
import { ILogService } from '../../../../platform/log/common/log.js';
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
import { isLocalhost } from '../../positronHelp/browser/utils.js';
Expand All @@ -28,6 +28,8 @@ import { basename } from '../../../../base/common/path.js';
import { IExtensionService } from '../../../services/extensions/common/extensions.js';
import { IEditorService } from '../../../services/editor/common/editorService.js';
import { Schemas } from '../../../../base/common/network.js';
import { INotificationService } from '../../../../platform/notification/common/notification.js';
import { localize } from '../../../../nls.js';

/**
* Positron preview service; keeps track of the set of active previews and
Expand Down Expand Up @@ -56,6 +58,7 @@ export class PositronPreviewService extends Disposable implements IPositronPrevi
@IRuntimeSessionService private readonly _runtimeSessionService: IRuntimeSessionService,
@ILogService private readonly _logService: ILogService,
@IOpenerService private readonly _openerService: IOpenerService,
@INotificationService private readonly _notificationService: INotificationService,
@IPositronNotebookOutputWebviewService private readonly _notebookOutputWebviewService: IPositronNotebookOutputWebviewService,
@IExtensionService private readonly _extensionService: IExtensionService,
@IEditorService private readonly _editorService: IEditorService
Expand Down Expand Up @@ -88,8 +91,12 @@ export class PositronPreviewService extends Disposable implements IPositronPrevi

if (e.event.name === UiFrontendEvent.ShowHtmlFile) {
const data = e.event.data as IShowHtmlUriEvent;
if (!data.event.is_plot) {
if (data.event.destination === ShowHtmlFileDestination.Viewer) {
this.handleShowHtmlFileEvent(session, data);
} else if (data.event.destination === ShowHtmlFileDestination.Editor) {
this.openEditor(data.uri, data.event.title).catch(err => {
this._notificationService.error(localize('positronPreviewFailedToOpenHtmlFileInEditor', "Failed to open {1} in editor: {0}", data.uri.toString(), err.message));
});
}
} else {
this.handleShowUrlEvent(session, e.event.data as ShowUrlEvent);
Expand Down Expand Up @@ -321,7 +328,7 @@ export class PositronPreviewService extends Disposable implements IPositronPrevi
const evt: ShowHtmlFileEvent = {
height: 0,
title: basename(htmlpath),
is_plot: false,
destination: ShowHtmlFileDestination.Viewer,
path: htmlpath,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { IRuntimeClientInstance, RuntimeClientState } from './languageRuntimeClientInstance.js';
import { BusyEvent, ClearConsoleEvent, UiFrontendEvent, OpenEditorEvent, OpenWorkspaceEvent, PromptStateEvent, ShowMessageEvent, WorkingDirectoryEvent, ShowUrlEvent, SetEditorSelectionsEvent, ShowHtmlFileEvent, OpenWithSystemEvent, ClearWebviewPreloadsEvent } from './positronUiComm.js';
import { BusyEvent, ClearConsoleEvent, UiFrontendEvent, OpenEditorEvent, OpenWorkspaceEvent, PromptStateEvent, ShowMessageEvent, WorkingDirectoryEvent, ShowUrlEvent, SetEditorSelectionsEvent, ShowHtmlFileEvent, OpenWithSystemEvent, ClearWebviewPreloadsEvent, ShowHtmlFileDestination } from './positronUiComm.js';
import { PositronUiCommInstance } from './positronUiCommInstance.js';
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
import { URI } from '../../../../base/common/uri.js';
Expand Down Expand Up @@ -175,17 +175,17 @@ export class UiClientInstance extends Disposable {
// Start an HTML proxy server for the file
const uri = await this.startHtmlProxyServer(e.path);

if (isWeb) {
if (isWeb && e.destination === ShowHtmlFileDestination.Plot) {
// In Web mode, we can't show interactive plots in the Plots
// pane.
e.is_plot = false;
} else if (e.is_plot) {
// pane, so show them in the Viewer tab instead.
e.destination = ShowHtmlFileDestination.Viewer;
} else if (e.destination === ShowHtmlFileDestination.Plot) {
// Check the configuration to see if we should open the plot
// in the Viewer tab. If so, clear the `is_plot` flag so that
// in the Viewer tab. If so, update the destination so that
// we open the file in the Viewer.
const openInViewer = this._configurationService.getValue<boolean>(POSITRON_PREVIEW_PLOTS_IN_VIEWER);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drive by thought, should POSITRON_PREVIEW_PLOTS_IN_VIEWER allow opening in an editor as well?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe? It seems a little severe to open up an editor for every new plot, but we could allow folks to do that if they want to.

if (openInViewer) {
e.is_plot = false;
e.destination = ShowHtmlFileDestination.Viewer;
}
}

Expand Down
21 changes: 16 additions & 5 deletions src/vs/workbench/services/languageRuntime/common/positronUiComm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,15 @@ export enum OpenEditorKind {
Uri = 'uri'
}

/**
* Possible values for Destination in ShowHtmlFile
*/
export enum ShowHtmlFileDestination {
Plot = 'plot',
Viewer = 'viewer',
Editor = 'editor'
}

/**
* Possible values for Type in PreviewSource
*/
Expand Down Expand Up @@ -503,9 +512,10 @@ export interface ShowHtmlFileParams {
title: string;

/**
* Whether the HTML file is a plot-like object
* Where the file should be shown in Positron: as an interactive plot, in
* the viewer pane, or in a new editor tab.
*/
is_plot: boolean;
destination: ShowHtmlFileDestination;

/**
* The desired height of the HTML viewer, in pixels. The special value 0
Expand Down Expand Up @@ -666,9 +676,10 @@ export interface ShowHtmlFileEvent {
title: string;

/**
* Whether the HTML file is a plot-like object
* Where the file should be shown in Positron: as an interactive plot, in
* the viewer pane, or in a new editor tab.
*/
is_plot: boolean;
destination: ShowHtmlFileDestination;

/**
* The desired height of the HTML viewer, in pixels. The special value 0
Expand Down Expand Up @@ -958,7 +969,7 @@ export class PositronUiComm extends PositronBaseComm {
this.onDidOpenWorkspace = super.createEventEmitter('open_workspace', ['path', 'new_window']);
this.onDidSetEditorSelections = super.createEventEmitter('set_editor_selections', ['selections']);
this.onDidShowUrl = super.createEventEmitter('show_url', ['url', 'source']);
this.onDidShowHtmlFile = super.createEventEmitter('show_html_file', ['path', 'title', 'is_plot', 'height']);
this.onDidShowHtmlFile = super.createEventEmitter('show_html_file', ['path', 'title', 'destination', 'height']);
this.onDidOpenWithSystem = super.createEventEmitter('open_with_system', ['path']);
this.onDidClearWebviewPreloads = super.createEventEmitter('clear_webview_preloads', []);
}
Expand Down
Loading