From 8fb92a77149839c852bf1a752630c1d34a5cea64 Mon Sep 17 00:00:00 2001 From: isabelizimm Date: Tue, 2 Apr 2024 16:30:49 -0400 Subject: [PATCH 1/4] open viewer on terminal links --- .../positron-dts/positron.d.ts | 11 ++++++ .../src/client/positron/extension.ts | 37 +++++++++++++++++++ 2 files changed, 48 insertions(+) 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..78daf96047b 100644 --- a/extensions/positron-python/src/client/positron/extension.ts +++ b/extensions/positron-python/src/client/positron/extension.ts @@ -11,6 +11,13 @@ import { IServiceContainer } from '../ioc/types'; import { traceError, traceInfo } from '../logging'; import { PythonRuntimeManager } from './manager'; import { createPythonRuntimeMetadata } from './runtime'; +import * as vscode from 'vscode'; + +export const UrlRegex = /(http|https):\/\/([\w_-]+(?:(?:\.[\w_-]+)+)|(localhost))([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])/gi; + +interface CustomTerminalLink extends vscode.TerminalLink { + data: string; +} export async function activatePositron( activatedPromise: Promise, @@ -29,6 +36,36 @@ export async function activatePositron( traceInfo('activatePositron: awaiting extension activation'); await activatedPromise; + vscode.window.registerTerminalLinkProvider({ + provideTerminalLinks: (context: vscode.TerminalLinkContext, _token: vscode.CancellationToken) => { + // Detect the first instance of the word "link" if it exists and linkify it + const matches = [...context.line.matchAll(UrlRegex)]; + if (matches.length === 0) { + return []; + } + + return matches.map((match) => { + const line = context.line; + + const startIndex = line.indexOf(match[0]); + + const uri = vscode.Uri.parse(match[0]); + positron.window.previewUrl(uri); + + return { + startIndex, + length: match[0].length, + tooltip: 'Open in Viewer', + data: match[0], + }; + }); + }, + handleTerminalLink: (link: CustomTerminalLink) => { + const uri = vscode.Uri.parse(link.data); + positron.window.previewUrl(uri); + }, + }); + const registerRuntime = async (interpreterPath: string) => { if (!manager.registeredPythonRuntimes.has(interpreterPath)) { // Get the interpreter corresponding to the new runtime. From e0677a19de4da301009efec515686710362b7980 Mon Sep 17 00:00:00 2001 From: isabelizimm Date: Tue, 2 Apr 2024 17:47:06 -0400 Subject: [PATCH 2/4] dont open links so aggressively --- .../src/client/positron/extension.ts | 39 ++------------- .../src/client/positron/linkProvider.ts | 48 +++++++++++++++++++ 2 files changed, 51 insertions(+), 36 deletions(-) create mode 100644 extensions/positron-python/src/client/positron/linkProvider.ts diff --git a/extensions/positron-python/src/client/positron/extension.ts b/extensions/positron-python/src/client/positron/extension.ts index 78daf96047b..de50ad93ee4 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,13 +12,7 @@ import { IServiceContainer } from '../ioc/types'; import { traceError, traceInfo } from '../logging'; import { PythonRuntimeManager } from './manager'; import { createPythonRuntimeMetadata } from './runtime'; -import * as vscode from 'vscode'; - -export const UrlRegex = /(http|https):\/\/([\w_-]+(?:(?:\.[\w_-]+)+)|(localhost))([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])/gi; - -interface CustomTerminalLink extends vscode.TerminalLink { - data: string; -} +import { provider } from './linkProvider'; export async function activatePositron( activatedPromise: Promise, @@ -36,35 +31,7 @@ export async function activatePositron( traceInfo('activatePositron: awaiting extension activation'); await activatedPromise; - vscode.window.registerTerminalLinkProvider({ - provideTerminalLinks: (context: vscode.TerminalLinkContext, _token: vscode.CancellationToken) => { - // Detect the first instance of the word "link" if it exists and linkify it - const matches = [...context.line.matchAll(UrlRegex)]; - if (matches.length === 0) { - return []; - } - - return matches.map((match) => { - const line = context.line; - - const startIndex = line.indexOf(match[0]); - - const uri = vscode.Uri.parse(match[0]); - positron.window.previewUrl(uri); - - return { - startIndex, - length: match[0].length, - tooltip: 'Open in Viewer', - data: match[0], - }; - }); - }, - handleTerminalLink: (link: CustomTerminalLink) => { - const uri = vscode.Uri.parse(link.data); - positron.window.previewUrl(uri); - }, - }); + vscode.window.registerTerminalLinkProvider(provider); const registerRuntime = async (interpreterPath: string) => { if (!manager.registeredPythonRuntimes.has(interpreterPath)) { 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..8b3075f314d --- /dev/null +++ b/extensions/positron-python/src/client/positron/linkProvider.ts @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * 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'; + +const UrlRegex = /(http|https):\/\/([\w_-]+(?:(?:\.[\w_-]+)+)|(localhost))([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])/gi; + +interface PositronTerminalLink extends vscode.TerminalLink { + data: string; +} + +const _links: string[] = []; + +export const provider = { + provideTerminalLinks: (context: vscode.TerminalLinkContext, _token: vscode.CancellationToken) => { + const matches = [...context.line.matchAll(UrlRegex)]; + if (matches.length === 0) { + return []; + } + + return matches.map((match) => { + const startIndex = context.line.indexOf(match[0]); + + if (!_links.includes(match[0])) { + const uri = vscode.Uri.parse(match[0]); + positron.window.previewUrl(uri); + } + // fix, people will want to open the same link multiple times + // are we able to get context of the viewer? + // or use some sort of other context from link/application/process lifespan? + _links.push(match[0]); + + return { + startIndex, + length: match[0].length, + tooltip: 'Open in Viewer', + data: match[0], + } as PositronTerminalLink; + }); + }, + handleTerminalLink: (link: PositronTerminalLink) => { + const uri = vscode.Uri.parse(link.data); + positron.window.previewUrl(uri); + }, +}; From c1692d2ecbbada9bf9725cdada18a6d2e1ba7b96 Mon Sep 17 00:00:00 2001 From: isabelizimm Date: Thu, 4 Apr 2024 17:01:13 -0400 Subject: [PATCH 3/4] match for pid + url --- .../src/client/positron/extension.ts | 4 +- .../src/client/positron/linkProvider.ts | 94 +++++++++++++------ 2 files changed, 67 insertions(+), 31 deletions(-) diff --git a/extensions/positron-python/src/client/positron/extension.ts b/extensions/positron-python/src/client/positron/extension.ts index de50ad93ee4..ae58f4dfcc2 100644 --- a/extensions/positron-python/src/client/positron/extension.ts +++ b/extensions/positron-python/src/client/positron/extension.ts @@ -12,7 +12,7 @@ import { IServiceContainer } from '../ioc/types'; import { traceError, traceInfo } from '../logging'; import { PythonRuntimeManager } from './manager'; import { createPythonRuntimeMetadata } from './runtime'; -import { provider } from './linkProvider'; +import { provideTerminalLinks, handleTerminalLink } from './linkProvider'; export async function activatePositron( activatedPromise: Promise, @@ -31,7 +31,7 @@ export async function activatePositron( traceInfo('activatePositron: awaiting extension activation'); await activatedPromise; - vscode.window.registerTerminalLinkProvider(provider); + vscode.window.registerTerminalLinkProvider({ provideTerminalLinks, handleTerminalLink }); const registerRuntime = async (interpreterPath: string) => { if (!manager.registeredPythonRuntimes.has(interpreterPath)) { diff --git a/extensions/positron-python/src/client/positron/linkProvider.ts b/extensions/positron-python/src/client/positron/linkProvider.ts index 8b3075f314d..627406c7dcb 100644 --- a/extensions/positron-python/src/client/positron/linkProvider.ts +++ b/extensions/positron-python/src/client/positron/linkProvider.ts @@ -6,43 +6,79 @@ import * as positron from 'positron'; import * as vscode from 'vscode'; -const UrlRegex = /(http|https):\/\/([\w_-]+(?:(?:\.[\w_-]+)+)|(localhost))([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])/gi; +// 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: string[] = []; +const _links: Map, string> = new Map(); -export const provider = { - provideTerminalLinks: (context: vscode.TerminalLinkContext, _token: vscode.CancellationToken) => { - const matches = [...context.line.matchAll(UrlRegex)]; - if (matches.length === 0) { - return []; - } +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])); - return matches.map((match) => { - const startIndex = context.line.indexOf(match[0]); + // set pid to latest localhost address + _links.set(pid, match[0]); - if (!_links.includes(match[0])) { - const uri = vscode.Uri.parse(match[0]); - positron.window.previewUrl(uri); + return { + startIndex, + length: match[0].length, + tooltip: 'Open link in Viewer', + data: match[0], + openInViewer: true, + } as PositronTerminalLink; } - // fix, people will want to open the same link multiple times - // are we able to get context of the viewer? - // or use some sort of other context from link/application/process lifespan? - _links.push(match[0]); - - return { - startIndex, - length: match[0].length, - tooltip: 'Open in Viewer', - data: match[0], - } as PositronTerminalLink; - }); - }, - handleTerminalLink: (link: 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)); + } +} From 8427b72559db2520da62e9f02befbf4bfbc738bc Mon Sep 17 00:00:00 2001 From: isabelizimm Date: Thu, 4 Apr 2024 17:01:59 -0400 Subject: [PATCH 4/4] handle localhost vs external --- extensions/positron-python/src/client/positron/linkProvider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/positron-python/src/client/positron/linkProvider.ts b/extensions/positron-python/src/client/positron/linkProvider.ts index 627406c7dcb..9899888e52d 100644 --- a/extensions/positron-python/src/client/positron/linkProvider.ts +++ b/extensions/positron-python/src/client/positron/linkProvider.ts @@ -45,7 +45,7 @@ export function provideTerminalLinks( // if localhost, preview through viewer if (localHostsPattern.test(match[0])) { - const pid = context.terminal.processId + const pid = context.terminal.processId; if (!_links.has(pid) || _links.get(pid) !== match[0]) { positron.window.previewUrl(vscode.Uri.parse(match[0]));