From f2513b5ea75e33b9727fe8e1aa50d931c4d0e735 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 31 Mar 2025 14:36:59 -0700 Subject: [PATCH] Add commands to quickly switch between svg preview and text Fixes #245180 --- extensions/media-preview/package.json | 34 ++++ extensions/media-preview/package.nls.json | 4 +- extensions/media-preview/src/audioPreview.ts | 6 +- .../media-preview/src/imagePreview/index.ts | 62 ++++++-- extensions/media-preview/src/mediaPreview.ts | 42 ++--- extensions/media-preview/src/videoPreview.ts | 6 +- .../browser/parts/editor/editorCommands.ts | 145 ++++++++++-------- 7 files changed, 193 insertions(+), 106 deletions(-) diff --git a/extensions/media-preview/package.json b/extensions/media-preview/package.json index 7e2b70293fc..02b0134e4cf 100644 --- a/extensions/media-preview/package.json +++ b/extensions/media-preview/package.json @@ -90,6 +90,18 @@ "command": "imagePreview.copyImage", "title": "%command.copyImage%", "category": "Image Preview" + }, + { + "command": "imagePreview.reopenAsPreview", + "title": "%command.reopenAsPreview%", + "category": "Image Preview", + "icon": "$(preview)" + }, + { + "command": "imagePreview.reopenAsText", + "title": "%command.reopenAsText%", + "category": "Image Preview", + "icon": "$(go-to-file)" } ], "menus": { @@ -107,6 +119,16 @@ { "command": "imagePreview.copyImage", "when": "false" + }, + { + "command": "imagePreview.reopenAsPreview", + "when": "activeEditor == workbench.editors.files.textFileEditor && resourceExtname == '.svg'", + "group": "navigation" + }, + { + "command": "imagePreview.reopenAsText", + "when": "activeCustomEditorId == 'imagePreview.previewEditor' && resourceExtname == '.svg'", + "group": "navigation" } ], "webview/context": [ @@ -114,6 +136,18 @@ "command": "imagePreview.copyImage", "when": "webviewId == 'imagePreview.previewEditor'" } + ], + "editor/title": [ + { + "command": "imagePreview.reopenAsPreview", + "when": "editorFocus && resourceExtname == '.svg'", + "group": "navigation" + }, + { + "command": "imagePreview.reopenAsText", + "when": "activeCustomEditorId == 'imagePreview.previewEditor' && resourceExtname == '.svg'", + "group": "navigation" + } ] } }, diff --git a/extensions/media-preview/package.nls.json b/extensions/media-preview/package.nls.json index c45e1e2613b..920ced76435 100644 --- a/extensions/media-preview/package.nls.json +++ b/extensions/media-preview/package.nls.json @@ -8,5 +8,7 @@ "videoPreviewerLoop": "Loop videos over again automatically.", "command.zoomIn": "Zoom in", "command.zoomOut": "Zoom out", - "command.copyImage": "Copy" + "command.copyImage": "Copy", + "command.reopenAsPreview": "Reopen as image preview", + "command.reopenAsText": "Reopen as source text" } diff --git a/extensions/media-preview/src/audioPreview.ts b/extensions/media-preview/src/audioPreview.ts index e21a4189d7b..5058f7e978e 100644 --- a/extensions/media-preview/src/audioPreview.ts +++ b/extensions/media-preview/src/audioPreview.ts @@ -54,12 +54,12 @@ class AudioPreview extends MediaPreview { protected async getWebviewContents(): Promise { const version = Date.now().toString(); const settings = { - src: await this.getResourcePath(this.webviewEditor, this.resource, version), + src: await this.getResourcePath(this._webviewEditor, this._resource, version), }; const nonce = getNonce(); - const cspSource = this.webviewEditor.webview.cspSource; + const cspSource = this._webviewEditor.webview.cspSource; return /* html */` @@ -104,7 +104,7 @@ class AudioPreview extends MediaPreview { } private extensionResource(...parts: string[]) { - return this.webviewEditor.webview.asWebviewUri(vscode.Uri.joinPath(this.extensionRoot, ...parts)); + return this._webviewEditor.webview.asWebviewUri(vscode.Uri.joinPath(this.extensionRoot, ...parts)); } } diff --git a/extensions/media-preview/src/imagePreview/index.ts b/extensions/media-preview/src/imagePreview/index.ts index e0c605c2a6e..b405cd652c4 100644 --- a/extensions/media-preview/src/imagePreview/index.ts +++ b/extensions/media-preview/src/imagePreview/index.ts @@ -11,7 +11,7 @@ import { SizeStatusBarEntry } from './sizeStatusBarEntry'; import { Scale, ZoomStatusBarEntry } from './zoomStatusBarEntry'; -export class PreviewManager implements vscode.CustomReadonlyEditorProvider { +export class ImagePreviewManager implements vscode.CustomReadonlyEditorProvider { public static readonly viewType = 'imagePreview.previewEditor'; @@ -48,7 +48,20 @@ export class PreviewManager implements vscode.CustomReadonlyEditorProvider { }); } - public get activePreview() { return this._activePreview; } + public get activePreview() { + return this._activePreview; + } + + public getPreviewFor(resource: vscode.Uri, viewColumn?: vscode.ViewColumn): ImagePreview | undefined { + for (const preview of this._previews) { + if (preview.resource.toString() === resource.toString()) { + if (!viewColumn || preview.viewColumn === viewColumn) { + return preview; + } + } + } + return undefined; + } private setActivePreview(value: ImagePreview | undefined): void { this._activePreview = value; @@ -94,12 +107,12 @@ class ImagePreview extends MediaPreview { this._register(zoomStatusBarEntry.onDidChangeScale(e => { if (this.previewState === PreviewState.Active) { - this.webviewEditor.webview.postMessage({ type: 'setScale', scale: e.scale }); + this._webviewEditor.webview.postMessage({ type: 'setScale', scale: e.scale }); } })); this._register(webviewEditor.onDidChangeViewState(() => { - this.webviewEditor.webview.postMessage({ type: 'setActive', value: this.webviewEditor.active }); + this._webviewEditor.webview.postMessage({ type: 'setActive', value: this._webviewEditor.active }); })); this._register(webviewEditor.onDidDispose(() => { @@ -121,22 +134,26 @@ class ImagePreview extends MediaPreview { this.zoomStatusBarEntry.hide(this); } + public get viewColumn() { + return this._webviewEditor.viewColumn; + } + public zoomIn() { if (this.previewState === PreviewState.Active) { - this.webviewEditor.webview.postMessage({ type: 'zoomIn' }); + this._webviewEditor.webview.postMessage({ type: 'zoomIn' }); } } public zoomOut() { if (this.previewState === PreviewState.Active) { - this.webviewEditor.webview.postMessage({ type: 'zoomOut' }); + this._webviewEditor.webview.postMessage({ type: 'zoomOut' }); } } public copyImage() { if (this.previewState === PreviewState.Active) { - this.webviewEditor.reveal(); - this.webviewEditor.webview.postMessage({ type: 'copyImage' }); + this._webviewEditor.reveal(); + this._webviewEditor.webview.postMessage({ type: 'copyImage' }); } } @@ -147,7 +164,7 @@ class ImagePreview extends MediaPreview { return; } - if (this.webviewEditor.active) { + if (this._webviewEditor.active) { this.sizeStatusBarEntry.show(this, this._imageSize || ''); this.zoomStatusBarEntry.show(this, this._imageZoom || 'fit'); } else { @@ -155,20 +172,21 @@ class ImagePreview extends MediaPreview { this.zoomStatusBarEntry.hide(this); } } + protected override async render(): Promise { await super.render(); - this.webviewEditor.webview.postMessage({ type: 'setActive', value: this.webviewEditor.active }); + this._webviewEditor.webview.postMessage({ type: 'setActive', value: this._webviewEditor.active }); } protected override async getWebviewContents(): Promise { const version = Date.now().toString(); const settings = { - src: await this.getResourcePath(this.webviewEditor, this.resource, version), + src: await this.getResourcePath(this._webviewEditor, this._resource, version), }; const nonce = getNonce(); - const cspSource = this.webviewEditor.webview.cspSource; + const cspSource = this._webviewEditor.webview.cspSource; return /* html */` @@ -212,7 +230,12 @@ class ImagePreview extends MediaPreview { } private extensionResource(...parts: string[]) { - return this.webviewEditor.webview.asWebviewUri(vscode.Uri.joinPath(this.extensionRoot, ...parts)); + return this._webviewEditor.webview.asWebviewUri(vscode.Uri.joinPath(this.extensionRoot, ...parts)); + } + + public async reopenAsText() { + await vscode.commands.executeCommand('reopenActiveEditorWith', 'default'); + this._webviewEditor.dispose(); } } @@ -226,9 +249,9 @@ export function registerImagePreviewSupport(context: vscode.ExtensionContext, bi const zoomStatusBarEntry = new ZoomStatusBarEntry(); disposables.push(zoomStatusBarEntry); - const previewManager = new PreviewManager(context.extensionUri, sizeStatusBarEntry, binarySizeStatusBarEntry, zoomStatusBarEntry); + const previewManager = new ImagePreviewManager(context.extensionUri, sizeStatusBarEntry, binarySizeStatusBarEntry, zoomStatusBarEntry); - disposables.push(vscode.window.registerCustomEditorProvider(PreviewManager.viewType, previewManager, { + disposables.push(vscode.window.registerCustomEditorProvider(ImagePreviewManager.viewType, previewManager, { supportsMultipleEditorsPerDocument: true, })); @@ -244,5 +267,14 @@ export function registerImagePreviewSupport(context: vscode.ExtensionContext, bi previewManager.activePreview?.copyImage(); })); + disposables.push(vscode.commands.registerCommand('imagePreview.reopenAsText', async () => { + return previewManager.activePreview?.reopenAsText(); + })); + + disposables.push(vscode.commands.registerCommand('imagePreview.reopenAsPreview', async () => { + + await vscode.commands.executeCommand('reopenActiveEditorWith', ImagePreviewManager.viewType); + })); + return vscode.Disposable.from(...disposables); } diff --git a/extensions/media-preview/src/mediaPreview.ts b/extensions/media-preview/src/mediaPreview.ts index 26d1e25dbaa..ccf83166e29 100644 --- a/extensions/media-preview/src/mediaPreview.ts +++ b/extensions/media-preview/src/mediaPreview.ts @@ -8,8 +8,8 @@ import { Utils } from 'vscode-uri'; import { BinarySizeStatusBarEntry } from './binarySizeStatusBarEntry'; import { Disposable } from './util/dispose'; -export function reopenAsText(resource: vscode.Uri, viewColumn: vscode.ViewColumn | undefined) { - vscode.commands.executeCommand('vscode.openWith', resource, 'default', viewColumn); +export async function reopenAsText(resource: vscode.Uri, viewColumn: vscode.ViewColumn | undefined): Promise { + await vscode.commands.executeCommand('vscode.openWith', resource, 'default', viewColumn); } export const enum PreviewState { @@ -25,52 +25,56 @@ export abstract class MediaPreview extends Disposable { constructor( extensionRoot: vscode.Uri, - protected readonly resource: vscode.Uri, - protected readonly webviewEditor: vscode.WebviewPanel, - private readonly binarySizeStatusBarEntry: BinarySizeStatusBarEntry, + protected readonly _resource: vscode.Uri, + protected readonly _webviewEditor: vscode.WebviewPanel, + private readonly _binarySizeStatusBarEntry: BinarySizeStatusBarEntry, ) { super(); - webviewEditor.webview.options = { + _webviewEditor.webview.options = { enableScripts: true, enableForms: false, localResourceRoots: [ - Utils.dirname(resource), + Utils.dirname(_resource), extensionRoot, ] }; - this._register(webviewEditor.onDidChangeViewState(() => { + this._register(_webviewEditor.onDidChangeViewState(() => { this.updateState(); })); - this._register(webviewEditor.onDidDispose(() => { + this._register(_webviewEditor.onDidDispose(() => { this.previewState = PreviewState.Disposed; this.dispose(); })); - const watcher = this._register(vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(resource, '*'))); + const watcher = this._register(vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(_resource, '*'))); this._register(watcher.onDidChange(e => { - if (e.toString() === this.resource.toString()) { + if (e.toString() === this._resource.toString()) { this.updateBinarySize(); this.render(); } })); this._register(watcher.onDidDelete(e => { - if (e.toString() === this.resource.toString()) { - this.webviewEditor.dispose(); + if (e.toString() === this._resource.toString()) { + this._webviewEditor.dispose(); } })); } public override dispose() { super.dispose(); - this.binarySizeStatusBarEntry.hide(this); + this._binarySizeStatusBarEntry.hide(this); + } + + public get resource() { + return this._resource; } protected updateBinarySize() { - vscode.workspace.fs.stat(this.resource).then(({ size }) => { + vscode.workspace.fs.stat(this._resource).then(({ size }) => { this._binarySize = size; this.updateState(); }); @@ -86,7 +90,7 @@ export abstract class MediaPreview extends Disposable { return; } - this.webviewEditor.webview.html = content; + this._webviewEditor.webview.html = content; } protected abstract getWebviewContents(): Promise; @@ -96,11 +100,11 @@ export abstract class MediaPreview extends Disposable { return; } - if (this.webviewEditor.active) { + if (this._webviewEditor.active) { this.previewState = PreviewState.Active; - this.binarySizeStatusBarEntry.show(this, this._binarySize); + this._binarySizeStatusBarEntry.show(this, this._binarySize); } else { - this.binarySizeStatusBarEntry.hide(this); + this._binarySizeStatusBarEntry.hide(this); this.previewState = PreviewState.Visible; } } diff --git a/extensions/media-preview/src/videoPreview.ts b/extensions/media-preview/src/videoPreview.ts index efc6be76a4f..67012128cf7 100644 --- a/extensions/media-preview/src/videoPreview.ts +++ b/extensions/media-preview/src/videoPreview.ts @@ -56,14 +56,14 @@ class VideoPreview extends MediaPreview { const version = Date.now().toString(); const configurations = vscode.workspace.getConfiguration('mediaPreview.video'); const settings = { - src: await this.getResourcePath(this.webviewEditor, this.resource, version), + src: await this.getResourcePath(this._webviewEditor, this._resource, version), autoplay: configurations.get('autoPlay'), loop: configurations.get('loop'), }; const nonce = getNonce(); - const cspSource = this.webviewEditor.webview.cspSource; + const cspSource = this._webviewEditor.webview.cspSource; return /* html */` @@ -108,7 +108,7 @@ class VideoPreview extends MediaPreview { } private extensionResource(...parts: string[]) { - return this.webviewEditor.webview.asWebviewUri(vscode.Uri.joinPath(this.extensionRoot, ...parts)); + return this._webviewEditor.webview.asWebviewUri(vscode.Uri.joinPath(this.extensionRoot, ...parts)); } } diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index 83a724a79b1..9463dfc1de3 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -61,6 +61,7 @@ export const LOCK_GROUP_COMMAND_ID = 'workbench.action.lockEditorGroup'; export const UNLOCK_GROUP_COMMAND_ID = 'workbench.action.unlockEditorGroup'; export const SHOW_EDITORS_IN_GROUP = 'workbench.action.showEditorsInGroup'; export const REOPEN_WITH_COMMAND_ID = 'workbench.action.reopenWithEditor'; +export const REOPEN_ACTIVE_EDITOR_WITH_COMMAND_ID = 'reopenActiveEditorWith'; export const PIN_EDITOR_COMMAND_ID = 'workbench.action.pinEditor'; export const UNPIN_EDITOR_COMMAND_ID = 'workbench.action.unpinEditor'; @@ -867,74 +868,88 @@ function registerCloseEditorCommands() { weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: undefined, - handler: async (accessor, ...args: unknown[]) => { - const editorService = accessor.get(IEditorService); - const editorResolverService = accessor.get(IEditorResolverService); - const telemetryService = accessor.get(ITelemetryService); - - const resolvedContext = resolveCommandsContext(args, editorService, accessor.get(IEditorGroupsService), accessor.get(IListService)); - const editorReplacements = new Map(); - - for (const { group, editors } of resolvedContext.groupedEditors) { - for (const editor of editors) { - const untypedEditor = editor.toUntyped(); - if (!untypedEditor) { - return; // Resolver can only resolve untyped editors - } - - untypedEditor.options = { ...editorService.activeEditorPane?.options, override: EditorResolution.PICK }; - const resolvedEditor = await editorResolverService.resolveEditor(untypedEditor, group); - if (!isEditorInputWithOptionsAndGroup(resolvedEditor)) { - return; - } - - let editorReplacementsInGroup = editorReplacements.get(group); - if (!editorReplacementsInGroup) { - editorReplacementsInGroup = []; - editorReplacements.set(group, editorReplacementsInGroup); - } - - editorReplacementsInGroup.push({ - editor: editor, - replacement: resolvedEditor.editor, - forceReplaceDirty: editor.resource?.scheme === Schemas.untitled, - options: resolvedEditor.options - }); - - // Telemetry - type WorkbenchEditorReopenClassification = { - owner: 'rebornix'; - comment: 'Identify how a document is reopened'; - scheme: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'File system provider scheme for the resource' }; - ext: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'File extension for the resource' }; - from: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The editor view type the resource is switched from' }; - to: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The editor view type the resource is switched to' }; - }; - - type WorkbenchEditorReopenEvent = { - scheme: string; - ext: string; - from: string; - to: string; - }; - - telemetryService.publicLog2('workbenchEditorReopen', { - scheme: editor.resource?.scheme ?? '', - ext: editor.resource ? extname(editor.resource) : '', - from: editor.editorId ?? '', - to: resolvedEditor.editor.editorId ?? '' - }); - } - } - - // Replace editor with resolved one and make active - for (const [group, replacements] of editorReplacements) { - await group.replaceEditors(replacements); - await group.openEditor(replacements[0].replacement); - } + handler: (accessor, ...args: unknown[]) => { + return reopenEditorWith(accessor, EditorResolution.PICK, ...args); } }); + KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: REOPEN_ACTIVE_EDITOR_WITH_COMMAND_ID, + weight: KeybindingWeight.WorkbenchContrib, + when: undefined, + primary: undefined, + handler: (accessor, override?: string, ...args: unknown[]) => { + return reopenEditorWith(accessor, override ?? EditorResolution.PICK, ...args); + } + }); + + async function reopenEditorWith(accessor: ServicesAccessor, editorOverride: string | EditorResolution, ...args: unknown[]) { + const editorService = accessor.get(IEditorService); + const editorResolverService = accessor.get(IEditorResolverService); + const telemetryService = accessor.get(ITelemetryService); + + const resolvedContext = resolveCommandsContext(args, editorService, accessor.get(IEditorGroupsService), accessor.get(IListService)); + const editorReplacements = new Map(); + + for (const { group, editors } of resolvedContext.groupedEditors) { + for (const editor of editors) { + const untypedEditor = editor.toUntyped(); + if (!untypedEditor) { + return; // Resolver can only resolve untyped editors + } + + untypedEditor.options = { ...editorService.activeEditorPane?.options, override: editorOverride }; + const resolvedEditor = await editorResolverService.resolveEditor(untypedEditor, group); + if (!isEditorInputWithOptionsAndGroup(resolvedEditor)) { + return; + } + + let editorReplacementsInGroup = editorReplacements.get(group); + if (!editorReplacementsInGroup) { + editorReplacementsInGroup = []; + editorReplacements.set(group, editorReplacementsInGroup); + } + + editorReplacementsInGroup.push({ + editor: editor, + replacement: resolvedEditor.editor, + forceReplaceDirty: editor.resource?.scheme === Schemas.untitled, + options: resolvedEditor.options + }); + + // Telemetry + type WorkbenchEditorReopenClassification = { + owner: 'rebornix'; + comment: 'Identify how a document is reopened'; + scheme: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'File system provider scheme for the resource' }; + ext: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'File extension for the resource' }; + from: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The editor view type the resource is switched from' }; + to: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The editor view type the resource is switched to' }; + }; + + type WorkbenchEditorReopenEvent = { + scheme: string; + ext: string; + from: string; + to: string; + }; + + telemetryService.publicLog2('workbenchEditorReopen', { + scheme: editor.resource?.scheme ?? '', + ext: editor.resource ? extname(editor.resource) : '', + from: editor.editorId ?? '', + to: resolvedEditor.editor.editorId ?? '' + }); + } + } + + // Replace editor with resolved one and make active + for (const [group, replacements] of editorReplacements) { + await group.replaceEditors(replacements); + await group.openEditor(replacements[0].replacement); + } + } + CommandsRegistry.registerCommand(CLOSE_EDITORS_AND_GROUP_COMMAND_ID, async (accessor: ServicesAccessor, ...args: unknown[]) => { const editorGroupsService = accessor.get(IEditorGroupsService);