diff --git a/extensions/positron-python/python_files/posit/positron/tests/test_ui.py b/extensions/positron-python/python_files/posit/positron/tests/test_ui.py index 8a66d4d69a0e..d14f8706ba68 100644 --- a/extensions/positron-python/python_files/posit/positron/tests/test_ui.py +++ b/extensions/positron-python/python_files/posit/positron/tests/test_ui.py @@ -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. # @@ -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 @@ -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": ""} ) @@ -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", []), @@ -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) @@ -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 @@ -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( """\ @@ -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" diff --git a/extensions/positron-python/python_files/posit/positron/ui.py b/extensions/positron-python/python_files/posit/positron/ui.py index 4da1fa9121cf..e029a7325e48 100644 --- a/extensions/positron-python/python_files/posit/positron/ui.py +++ b/extensions/positron-python/python_files/posit/positron/ui.py @@ -21,6 +21,7 @@ CallMethodParams, CallMethodRequest, OpenEditorParams, + ShowHtmlFileDestination, ShowHtmlFileParams, ShowUrlParams, UiBackendMessageContent, @@ -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()) @@ -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 @@ -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(), diff --git a/extensions/positron-python/python_files/posit/positron/ui_comm.py b/extensions/positron-python/python_files/posit/positron/ui_comm.py index d04bd5610511..a483696da8cb 100644 --- a/extensions/positron-python/python_files/posit/positron/ui_comm.py +++ b/extensions/positron-python/python_files/posit/positron/ui_comm.py @@ -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): """ @@ -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( diff --git a/extensions/positron-r/package.json b/extensions/positron-r/package.json index 529922236ce9..4c51804c509f 100644 --- a/extensions/positron-r/package.json +++ b/extensions/positron-r/package.json @@ -955,7 +955,7 @@ }, "positron": { "binaryDependencies": { - "ark": "0.1.220" + "ark": "0.1.221" }, "minimumRVersion": "4.2.0", "minimumRenvVersion": "1.0.9" diff --git a/positron/comms/ui-frontend-openrpc.json b/positron/comms/ui-frontend-openrpc.json index d340825dc2f8..d5d12cc47bc5 100644 --- a/positron/comms/ui-frontend-openrpc.json +++ b/positron/comms/ui-frontend-openrpc.json @@ -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" + ] } }, { diff --git a/src/vs/workbench/contrib/positronPlots/browser/positronPlotsService.ts b/src/vs/workbench/contrib/positronPlots/browser/positronPlotsService.ts index a92fdfca1aff..33e5d3397c4b 100644 --- a/src/vs/workbench/contrib/positronPlots/browser/positronPlotsService.ts +++ b/src/vs/workbench/contrib/positronPlots/browser/positronPlotsService.ts @@ -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'; @@ -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); } } diff --git a/src/vs/workbench/contrib/positronPreview/browser/positronPreviewServiceImpl.ts b/src/vs/workbench/contrib/positronPreview/browser/positronPreviewServiceImpl.ts index acfc6217183b..d66326903368 100644 --- a/src/vs/workbench/contrib/positronPreview/browser/positronPreviewServiceImpl.ts +++ b/src/vs/workbench/contrib/positronPreview/browser/positronPreviewServiceImpl.ts @@ -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. *--------------------------------------------------------------------------------------------*/ @@ -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'; @@ -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 @@ -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 @@ -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); @@ -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, }; diff --git a/src/vs/workbench/services/languageRuntime/common/languageRuntimeUiClient.ts b/src/vs/workbench/services/languageRuntime/common/languageRuntimeUiClient.ts index e9c634810223..83a09a2164cf 100644 --- a/src/vs/workbench/services/languageRuntime/common/languageRuntimeUiClient.ts +++ b/src/vs/workbench/services/languageRuntime/common/languageRuntimeUiClient.ts @@ -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'; @@ -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(POSITRON_PREVIEW_PLOTS_IN_VIEWER); if (openInViewer) { - e.is_plot = false; + e.destination = ShowHtmlFileDestination.Viewer; } } diff --git a/src/vs/workbench/services/languageRuntime/common/positronUiComm.ts b/src/vs/workbench/services/languageRuntime/common/positronUiComm.ts index f56edf737d0d..ec13ea9cac55 100644 --- a/src/vs/workbench/services/languageRuntime/common/positronUiComm.ts +++ b/src/vs/workbench/services/languageRuntime/common/positronUiComm.ts @@ -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 */ @@ -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 @@ -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 @@ -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', []); }