Skip to content

Commit eacb46b

Browse files
authored
Add "open with system default application" event to UI comm (#8588)
Addresses #5486 Companion PR in ark: posit-dev/ark#877 This PR creates a new event in the UI comm that says "open this thing (file, presumably) with the default application according to the OS". The immediate motivating example is to allow R's `browseURL()` to work as expected in the Positron console. ### Release Notes <!-- Optionally, replace `N/A` with text to be included in the next release notes. The `N/A` bullets are ignored. If you refer to one or more Positron issues, these issues are used to collect information about the feature or bugfix, such as the relevant language pack as determined by Github labels of type `lang: `. The note will automatically be tagged with the language. These notes are typically filled by the Positron team. If you are an external contributor, you may ignore this section. --> #### New Features - `browseURL()` in R now delegates to the operating system's default opener for inputs that are not recognized as a web URL or an HTML file. #### Bug Fixes - N/A ### QA Notes In R, use `browseURL()` on a local filepath. Exercise a couple of files (meaning file types) and a folder. Here are examples taken from the "R Code" column of the table at #5486 (comment): | R Code | Expectation in Positron R Console, with this PR + ark PR | | -- | -- | |`browseURL(tempdir())` | opens folder in OS's notion of file explorer (e.g. Finder on macOS) | | `temp_csv_file <- tempfile()` <br>`write.csv(iris, temp_csv_file)` <br>`browseURL(temp_csv_file)` | opens the csv file in OS's default application (for text files? on macOS, I'm getting TextEdit) | | `temp_csv_file_with_ext <- tempfile(fileext = ".csv")` <br>`write.csv(iris, temp_csv_file_with_ext)` <br>`browseURL(temp_csv_file_with_ext)` | opens the csv file in OS's default application for `.csv` (VS Code for me, haha) | | `temp_html_file <- tempfile()` <br>`writeLines("<p>Hi!</p>", temp_html_file)` <br>`browseURL(temp_html_file)` | opens the html file in OS's default application (for text files? on macOS, I'm getting TextEdit) | | `temp_html_file_with_ext <- tempfile(fileext = ".html")` <br>`writeLines("<p>Hi!</p>", temp_html_file_with_ext)` <br>`browseURL(temp_html_file_with_ext)` | opens `.html` file in default external browser | When in doubt, the behaviour we seek is whatever happens in R in a plain terminal or in RStudio Console. We should definitely verify this on linux and Windows (I'm on macOS, so feel pretty confident about that). As for tests, I don't really know how we would do that 🤔 in this case. The whole point is to open a file in *some other* application and exactly what that is will vary by OS / user. Signed-off-by: Jennifer (Jenny) Bryan <jenny.f.bryan@gmail.com>
1 parent 10c15ee commit eacb46b

File tree

7 files changed

+88
-11
lines changed

7 files changed

+88
-11
lines changed

build/secrets/.secrets.baseline

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,6 @@
9494
"path": "detect_secrets.filters.common.is_baseline_file",
9595
"filename": "build/secrets/.secrets.baseline"
9696
},
97-
{
98-
"path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies",
99-
"min_level": 2
100-
},
10197
{
10298
"path": "detect_secrets.filters.heuristic.is_indirect_reference"
10399
},
@@ -1726,7 +1722,7 @@
17261722
"filename": "src/vs/workbench/services/languageRuntime/common/positronUiComm.ts",
17271723
"hashed_secret": "659b2a0d48dff4354af7e730d2138e52d57f5a6b",
17281724
"is_verified": false,
1729-
"line_number": 797,
1725+
"line_number": 819,
17301726
"is_secret": false
17311727
}
17321728
],

extensions/positron-python/python_files/posit/positron/ui_comm.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,9 @@ class UiFrontendEvent(str, enum.Enum):
261261
# Show an HTML file in Positron
262262
ShowHtmlFile = "show_html_file"
263263

264+
# Open a file or folder with the system default application
265+
OpenWithSystem = "open_with_system"
266+
264267
# Webview preloads should be flushed
265268
ClearWebviewPreloads = "clear_webview_preloads"
266269

@@ -509,6 +512,16 @@ class ShowHtmlFileParams(BaseModel):
509512
)
510513

511514

515+
class OpenWithSystemParams(BaseModel):
516+
"""
517+
Open a file or folder with the system default application
518+
"""
519+
520+
path: StrictStr = Field(
521+
description="The file path to open with the system default application",
522+
)
523+
524+
512525
EditorContext.update_forward_refs()
513526

514527
TextDocument.update_forward_refs()
@@ -562,3 +575,5 @@ class ShowHtmlFileParams(BaseModel):
562575
ShowUrlParams.update_forward_refs()
563576

564577
ShowHtmlFileParams.update_forward_refs()
578+
579+
OpenWithSystemParams.update_forward_refs()

positron/comms/ui-frontend-openrpc.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,20 @@
467467
}
468468
]
469469
},
470+
{
471+
"name": "open_with_system",
472+
"summary": "Open a file or folder with the system default application",
473+
"description": "Open a file or folder with the system default application",
474+
"params": [
475+
{
476+
"name": "path",
477+
"description": "The file path to open with the system default application",
478+
"schema": {
479+
"type": "string"
480+
}
481+
}
482+
]
483+
},
470484
{
471485
"name": "clear_webview_preloads",
472486
"summary": "Webview preloads should be flushed",

src/vs/workbench/api/browser/positron/mainThreadLanguageRuntime.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import { IPositronHelpService } from '../../../contrib/positronHelp/browser/posi
2929
import { INotebookService } from '../../../contrib/notebook/common/notebookService.js';
3030
import { IRuntimeClientEvent } from '../../../services/languageRuntime/common/languageRuntimeUiClient.js';
3131
import { URI } from '../../../../base/common/uri.js';
32-
import { BusyEvent, UiFrontendEvent, OpenEditorEvent, OpenWorkspaceEvent, PromptStateEvent, WorkingDirectoryEvent, ShowMessageEvent, SetEditorSelectionsEvent } from '../../../services/languageRuntime/common/positronUiComm.js';
32+
import { BusyEvent, UiFrontendEvent, OpenEditorEvent, OpenWorkspaceEvent, PromptStateEvent, WorkingDirectoryEvent, ShowMessageEvent, SetEditorSelectionsEvent, OpenWithSystemEvent } from '../../../services/languageRuntime/common/positronUiComm.js';
3333
import { IEditorService } from '../../../services/editor/common/editorService.js';
3434
import { IEditor } from '../../../../editor/common/editorCommon.js';
3535
import { Selection } from '../../../../editor/common/core/selection.js';
@@ -49,6 +49,7 @@ import { CodeAttributionSource, IConsoleCodeAttribution } from '../../../service
4949
import { QueryTableSummaryResult, Variable } from '../../../services/languageRuntime/common/positronVariablesComm.js';
5050
import { IPositronVariablesInstance } from '../../../services/positronVariables/common/interfaces/positronVariablesInstance.js';
5151
import { isWebviewPreloadMessage, isWebviewReplayMessage } from '../../../services/positronIPyWidgets/common/webviewPreloadUtils.js';
52+
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
5253

5354
/**
5455
* Represents a language runtime event (for example a message or state change)
@@ -142,7 +143,9 @@ class ExtHostLanguageRuntimeSessionAdapter extends Disposable implements ILangua
142143
private readonly _commandService: ICommandService,
143144
private readonly _notebookService: INotebookService,
144145
private readonly _editorService: IEditorService,
145-
private readonly _proxy: ExtHostLanguageRuntimeShape) {
146+
private readonly _proxy: ExtHostLanguageRuntimeShape,
147+
private readonly _openerService: IOpenerService
148+
) {
146149

147150
super();
148151

@@ -190,7 +193,7 @@ class ExtHostLanguageRuntimeSessionAdapter extends Disposable implements ILangua
190193
this._proxy.$notifyForegroundSessionChanged(session?.sessionId);
191194
}));
192195

193-
this._register(this._runtimeSessionService.onDidReceiveRuntimeEvent(globalEvent => {
196+
this._register(this._runtimeSessionService.onDidReceiveRuntimeEvent(async globalEvent => {
194197
// Ignore events for other sessions.
195198
if (globalEvent.session_id !== this.sessionId) {
196199
return;
@@ -247,6 +250,13 @@ class ExtHostLanguageRuntimeSessionAdapter extends Disposable implements ILangua
247250
const ws = ev.data as OpenWorkspaceEvent;
248251
const uri = URI.file(ws.path);
249252
this._commandService.executeCommand('vscode.openFolder', uri, ws.new_window);
253+
} else if (ev.name === UiFrontendEvent.OpenWithSystem) {
254+
// Open a file or folder with system default application
255+
const openWith = ev.data as OpenWithSystemEvent;
256+
const uri = URI.file(openWith.path);
257+
258+
// Use VS Code's opener service with external option
259+
await this._openerService.open(uri, { openExternal: true });
250260
} else if (ev.name === UiFrontendEvent.WorkingDirectory) {
251261
// Update current working directory
252262
const dir = ev.data as WorkingDirectoryEvent;
@@ -1393,7 +1403,8 @@ export class MainThreadLanguageRuntime
13931403
@ILogService private readonly _logService: ILogService,
13941404
@ICommandService private readonly _commandService: ICommandService,
13951405
@INotebookService private readonly _notebookService: INotebookService,
1396-
@IEditorService private readonly _editorService: IEditorService
1406+
@IEditorService private readonly _editorService: IEditorService,
1407+
@IOpenerService private readonly _openerService: IOpenerService
13971408
) {
13981409
// TODO@softwarenerd - We needed to find a central place where we could ensure that certain
13991410
// Positron services were up and running early in the application lifecycle. For now, this
@@ -1772,7 +1783,8 @@ export class MainThreadLanguageRuntime
17721783
this._commandService,
17731784
this._notebookService,
17741785
this._editorService,
1775-
this._proxy);
1786+
this._proxy,
1787+
this._openerService);
17761788
}
17771789

17781790
private findSession(handle: number): ExtHostLanguageRuntimeSessionAdapter {

src/vs/workbench/services/languageRuntime/common/languageRuntimeUiClient.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';
77
import { Emitter, Event } from '../../../../base/common/event.js';
88
import { IRuntimeClientInstance, RuntimeClientState } from './languageRuntimeClientInstance.js';
9-
import { BusyEvent, ClearConsoleEvent, UiFrontendEvent, OpenEditorEvent, OpenWorkspaceEvent, PromptStateEvent, ShowMessageEvent, WorkingDirectoryEvent, ShowUrlEvent, SetEditorSelectionsEvent, ShowHtmlFileEvent, ClearWebviewPreloadsEvent } from './positronUiComm.js';
9+
import { BusyEvent, ClearConsoleEvent, UiFrontendEvent, OpenEditorEvent, OpenWorkspaceEvent, PromptStateEvent, ShowMessageEvent, WorkingDirectoryEvent, ShowUrlEvent, SetEditorSelectionsEvent, ShowHtmlFileEvent, OpenWithSystemEvent, ClearWebviewPreloadsEvent } from './positronUiComm.js';
1010
import { PositronUiCommInstance } from './positronUiCommInstance.js';
1111
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
1212
import { URI } from '../../../../base/common/uri.js';
@@ -88,6 +88,7 @@ export class UiClientInstance extends Disposable {
8888
onDidWorkingDirectory: Event<WorkingDirectoryEvent>;
8989
onDidShowUrl: Event<ShowUrlEvent>;
9090
onDidShowHtmlFile: Event<IShowHtmlUriEvent>;
91+
onDidOpenWithSystem: Event<OpenWithSystemEvent>;
9192
onDidClearWebviewPreloads: Event<ClearWebviewPreloadsEvent>;
9293

9394
/** Emitter wrapper for Show URL events */
@@ -124,6 +125,7 @@ export class UiClientInstance extends Disposable {
124125
this.onDidWorkingDirectory = this._comm.onDidWorkingDirectory;
125126
this.onDidShowUrl = this._onDidShowUrlEmitter.event;
126127
this.onDidShowHtmlFile = this._onDidShowHtmlFileEmitter.event;
128+
this.onDidOpenWithSystem = this._comm.onDidOpenWithSystem;
127129
this.onDidClearWebviewPreloads = this._comm.onDidClearWebviewPreloads;
128130

129131
// Wrap the ShowUrl event to resolve incoming external URIs from the

src/vs/workbench/services/languageRuntime/common/positronUiComm.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,16 @@ export interface ShowHtmlFileParams {
445445
height: number;
446446
}
447447

448+
/**
449+
* Parameters for the OpenWithSystem method.
450+
*/
451+
export interface OpenWithSystemParams {
452+
/**
453+
* The file path to open with the system default application
454+
*/
455+
path: string;
456+
}
457+
448458
/**
449459
* Event: Change in backend's busy/idle status
450460
*/
@@ -588,6 +598,17 @@ export interface ShowHtmlFileEvent {
588598

589599
}
590600

601+
/**
602+
* Event: Open a file or folder with the system default application
603+
*/
604+
export interface OpenWithSystemEvent {
605+
/**
606+
* The file path to open with the system default application
607+
*/
608+
path: string;
609+
610+
}
611+
591612
/**
592613
* Event: Webview preloads should be flushed
593614
*/
@@ -787,6 +808,7 @@ export enum UiFrontendEvent {
787808
SetEditorSelections = 'set_editor_selections',
788809
ShowUrl = 'show_url',
789810
ShowHtmlFile = 'show_html_file',
811+
OpenWithSystem = 'open_with_system',
790812
ClearWebviewPreloads = 'clear_webview_preloads'
791813
}
792814

@@ -825,6 +847,7 @@ export class PositronUiComm extends PositronBaseComm {
825847
this.onDidSetEditorSelections = super.createEventEmitter('set_editor_selections', ['selections']);
826848
this.onDidShowUrl = super.createEventEmitter('show_url', ['url']);
827849
this.onDidShowHtmlFile = super.createEventEmitter('show_html_file', ['path', 'title', 'is_plot', 'height']);
850+
this.onDidOpenWithSystem = super.createEventEmitter('open_with_system', ['path']);
828851
this.onDidClearWebviewPreloads = super.createEventEmitter('clear_webview_preloads', []);
829852
}
830853

@@ -927,6 +950,12 @@ export class PositronUiComm extends PositronBaseComm {
927950
* Causes the HTML file to be shown in Positron.
928951
*/
929952
onDidShowHtmlFile: Event<ShowHtmlFileEvent>;
953+
/**
954+
* Open a file or folder with the system default application
955+
*
956+
* Open a file or folder with the system default application
957+
*/
958+
onDidOpenWithSystem: Event<OpenWithSystemEvent>;
930959
/**
931960
* Webview preloads should be flushed
932961
*

src/vs/workbench/services/runtimeSession/common/activeRuntimeSession.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,15 @@ export class ActiveRuntimeSession extends Disposable {
237237
}
238238
});
239239
}));
240+
this._register(uiClient.onDidOpenWithSystem(event => {
241+
this._onDidReceiveRuntimeEventEmitter.fire({
242+
session_id: sessionId,
243+
event: {
244+
name: UiFrontendEvent.OpenWithSystem,
245+
data: event
246+
}
247+
});
248+
}));
240249
this._register(uiClient.onDidClearWebviewPreloads(event => {
241250
this._onDidReceiveRuntimeEventEmitter.fire({
242251
session_id: sessionId,

0 commit comments

Comments
 (0)