0

I am building a VS Code extension that can create multiple webviews; each text editor document can have an associated webview. I want the user to be able to right-click in a webview and execute a command. The command must know which webview instance was clicked within—or active/focused when the command was issued from the command palette—to Do The Right Thing.

The context menu callback only receives {webview:<viewType>} as the argument.
How can I determine which webview the context menu originated in?

In case it helps, following is simplified extension code showing how I'm creating a new webview (and associated viewer manager) for each document.uri:

import * as vscode from 'vscode';
import {commands, TextEditor, window} from 'vscode';
import {Viewer} from './viewer';

export function activate(ctx: vscode.ExtensionContext) {
  let manager = new MyExtManager();
  const subs = ctx.subscriptions;
  subs.push(commands.registerCommand('myExt.show', () => {
    manager.showWebview(ctx.extensionUri);
  }));

  subs.push(commands.registerCommand('myExt.myCtxItem', (wv) => {
    // need some unique identifier here to locate original editor
    console.log(wv);
  }));
}

class MyExtManager {
  private viewerByDoc: Map<vscode.Uri, Viewer> = new Map();

  public showWebview(extensionURI: vscode.Uri) {
    const editor: TextEditor | undefined = window.activeTextEditor;
    const doc = editor?.document;

    if (doc && doc.languageId === "xml") {
      let viewerForDoc = this.viewerByDoc.get(doc.uri);
      if (viewerForDoc) {
        viewerForDoc.reveal();
      } else {
        const panel = vscode.window.createWebviewPanel(
          'myxmlviewer', `My Viewer for ${doc.uri}`,
          vscode.ViewColumn.Beside
        );
        panel.onDidDispose(() => this.viewerByDoc.delete(doc.uri));
        viewerForDoc = new Viewer(editor, panel)
        this.viewerByDoc.set(doc.uri, viewerForDoc);
      }
    }
  }
}

Things I've investigated:

  • I can't use vscode.window.active* because there is no collection for seeing active webviews like there is for activeTextEditor.
  • I cannot seem to find a way to get a callback when a WebviewPanel is revealed (given editor focus), or I could use an extension global to track the most recent.
  • I cannot seem to find a way to detect the opening of the context menu as a callback to the webview, or I could similarly store that as a global.

The only hack I can think of is to hijack the viewType parameter to be unique for each viewer instance. I can't find much about the intention of this parameter, but having unique values seems like an unintended use.


The context menu is constructed via these additions in the package.json file:

{
  "contributes": {
    "commands": [
      {
        "category": "XML Viewer",
        "command": "myExt.show",
        "title": "Open to the Side"
      },
      {
        "category": "XML Viewer",
        "command": "myExt.myCtxItem",
        "title": "Do a Thing Here"
      },
    ],
    "menus": {
      "webview/context": [
        {
          "command": "myExt.myCtxItem",
          "group": "xmlViewer"
        }
      ]
    }
  }
}
Phrogz
  • 296,393
  • 112
  • 651
  • 745
  • how do you construct the context menu – rioV8 Feb 22 '23 at 06:26
  • @rioV8 I've updated the question with information on how the menu is created at the bottom. – Phrogz Feb 22 '23 at 13:50
  • what if you capture mouse events in the webview, make a note in the webview of a context click, let it ripple up, in the `myExt.myCtxItem` interrogate the webviews who has had a context click, and also reset the flag in the webview – rioV8 Feb 22 '23 at 14:34

1 Answers1

0

Figured out a way:

When the WebviewPanel is created, register a onDidChangeViewState callback. Check if the panel is active and visible, and if so override a single variable with it.

public activeWebview: WebviewPanel | null = null;

// ...
panel.onDidChangeViewState(e => {
   if (panel.active && panel.visible) {
      this.activeWebview = panel;
   } else if (activeWebview===panel) {
      this.activeWebview = null;
   }
});

...and then use that in the extension's command registration:

subs.push(commands.registerCommand('myExt.myCtxItem', () => {
   if (manager.activeWebview) {
      // ...
   }    
}));

I'm actually doing far more work than the above so that the command is still available if the TextEditor associated with a webview is available…but that's outside the scope of this question.

Phrogz
  • 296,393
  • 112
  • 651
  • 745