diff --git a/extensions/positron-python/positron-dts/positron.d.ts b/extensions/positron-python/positron-dts/positron.d.ts index 77995183987..3606e91fa0f 100644 --- a/extensions/positron-python/positron-dts/positron.d.ts +++ b/extensions/positron-python/positron-dts/positron.d.ts @@ -1087,6 +1087,17 @@ declare module 'positron' { * Returns the current width of the console input, in characters. */ export function getConsoleWidth(): Thenable; + + /** + * Create and show a new preview panel for a URL. This is a convenience + * method that creates a new webview panel and sets its content to the + * given URL. + * + * @param url The URL to preview + * + * @return New preview panel. + */ + export function previewUrl(url: vscode.Uri): PreviewPanel; } namespace runtime { diff --git a/extensions/positron-python/src/client/positron/extension.ts b/extensions/positron-python/src/client/positron/extension.ts index fb1cfb6247a..ae58f4dfcc2 100644 --- a/extensions/positron-python/src/client/positron/extension.ts +++ b/extensions/positron-python/src/client/positron/extension.ts @@ -4,6 +4,7 @@ // eslint-disable-next-line import/no-unresolved import * as positron from 'positron'; +import * as vscode from 'vscode'; import { PythonExtension } from '../api/types'; import { IDisposableRegistry } from '../common/types'; import { IInterpreterService } from '../interpreter/contracts'; @@ -11,6 +12,7 @@ import { IServiceContainer } from '../ioc/types'; import { traceError, traceInfo } from '../logging'; import { PythonRuntimeManager } from './manager'; import { createPythonRuntimeMetadata } from './runtime'; +import { provideTerminalLinks, handleTerminalLink } from './linkProvider'; export async function activatePositron( activatedPromise: Promise, @@ -29,6 +31,8 @@ export async function activatePositron( traceInfo('activatePositron: awaiting extension activation'); await activatedPromise; + vscode.window.registerTerminalLinkProvider({ provideTerminalLinks, handleTerminalLink }); + const registerRuntime = async (interpreterPath: string) => { if (!manager.registeredPythonRuntimes.has(interpreterPath)) { // Get the interpreter corresponding to the new runtime. diff --git a/extensions/positron-python/src/client/positron/linkProvider.ts b/extensions/positron-python/src/client/positron/linkProvider.ts new file mode 100644 index 00000000000..9899888e52d --- /dev/null +++ b/extensions/positron-python/src/client/positron/linkProvider.ts @@ -0,0 +1,84 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2023 Posit Software, PBC. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// eslint-disable-next-line import/no-unresolved +import * as positron from 'positron'; +import * as vscode from 'vscode'; + +// Assuming localHosts is an array of localhost URLs +const localHosts: string[] = [ + 'localhost', + '127.0.0.1', + '[0:0:0:0:0:0:0:1]', + '[::1]', + '0.0.0.0', + '[0:0:0:0:0:0:0:0]', + '[::]', +]; + +const urlPattern = /(http|https):\/\/([\w_-]+(?:(?:\.[\w_-]+)+)|(localhost))([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])/gi; +const localHostsPattern = new RegExp( + `(?:https?:\/\/)(${localHosts.join('|')})([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~/+#-])`, + 'gi', +); + +interface PositronTerminalLink extends vscode.TerminalLink { + data: string; + openInViewer: boolean; +} + +const _links: Map, string> = new Map(); + +export function provideTerminalLinks( + context: vscode.TerminalLinkContext, + _token: vscode.CancellationToken, +): vscode.ProviderResult { + const matches = [...context.line.matchAll(urlPattern)]; + + if (matches.length === 0) { + return []; + } + + return matches.map((match) => { + const startIndex = context.line.indexOf(match[0]); + + // if localhost, preview through viewer + if (localHostsPattern.test(match[0])) { + const pid = context.terminal.processId; + if (!_links.has(pid) || _links.get(pid) !== match[0]) { + positron.window.previewUrl(vscode.Uri.parse(match[0])); + + // set pid to latest localhost address + _links.set(pid, match[0]); + + return { + startIndex, + length: match[0].length, + tooltip: 'Open link in Viewer', + data: match[0], + openInViewer: true, + } as PositronTerminalLink; + } + } + // otherwise, treat as external link + return { + startIndex, + length: match[0].length, + tooltip: 'Open link in browser', + data: match[0], + openInViewer: false, + } as PositronTerminalLink; + }); +} + +export function handleTerminalLink(link: PositronTerminalLink): void { + const config = vscode.workspace.getConfiguration('positron.viewer'); + + if (link.openInViewer && config.get('openLocalhostUrls')) { + const uri = vscode.Uri.parse(link.data); + positron.window.previewUrl(uri); + } else { + vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(link.data)); + } +}