[wasm] Introduce unregisterLanguageExtensionPlugin API.

This adds a new method `unregisterLanguageExtensionPlugin()` to the
`LanguageServicesAPI` exposed to DevTools extensions, which allows the
developer to unregister a previously registred language extension
plugin. This is useful for example when the configuration for an
extension changes and the front-end should reload the source mappings.

Drive-by-fix: Also make the registerLanguageExtensionPlugin and the
unregisterLanguageExtensionPlugin methods return promises, so that
the extensions can properly await / chain them.

Fixed: chromium:1151285
Bug: chromium:1151280, chromium:1041362, chromium:1083146
Change-Id: Ib62b70c717754d70cf827ba1a97fb5c3e358ee1f
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2552506
Reviewed-by: Philip Pfaffe <[email protected]>
Commit-Queue: Benedikt Meurer <[email protected]>
Auto-Submit: Benedikt Meurer <[email protected]>
diff --git a/front_end/extensions/ExtensionAPI.js b/front_end/extensions/ExtensionAPI.js
index 94e3b89..4ea1793 100644
--- a/front_end/extensions/ExtensionAPI.js
+++ b/front_end/extensions/ExtensionAPI.js
@@ -102,6 +102,11 @@
     GetInlinedFunctionRanges: 'getInlinedFunctionRanges',
     GetInlinedCalleesRanges: 'getInlinedCalleesRanges'
   };
+
+  /** @enum {string} */
+  apiPrivate.LanguageExtensionPluginEvents = {
+    UnregisteredLanguageExtensionPlugin: 'unregisteredLanguageExtensionPlugin'
+  };
 }
 
 /**
@@ -128,6 +133,7 @@
 
   const commands = apiPrivate.Commands;
   const languageExtensionPluginCommands = apiPrivate.LanguageExtensionPluginCommands;
+  const languageExtensionPluginEvents = apiPrivate.LanguageExtensionPluginEvents;
   const events = apiPrivate.Events;
   let userAction = false;
 
@@ -368,6 +374,7 @@
    * @constructor
    */
   function LanguageServicesAPIImpl() {
+    /** @type {!Map<*, !MessagePort>} */
     this._plugins = new Map();
   }
 
@@ -376,10 +383,11 @@
      * @param {*} plugin The language plugin instance to register.
      * @param {string} pluginName The plugin name
      * @param {{language: string, symbol_types: !Array<string>}} supportedScriptTypes Script language and debug symbol types supported by this extension.
+     * @return {!Promise<void>}
      */
-    registerLanguageExtensionPlugin: function(plugin, pluginName, supportedScriptTypes) {
+    registerLanguageExtensionPlugin: async function(plugin, pluginName, supportedScriptTypes) {
       if (this._plugins.has(plugin)) {
-        throw new Error('Tried to register a plugin twice');
+        throw new Error(`Tried to register plugin '${pluginName}' twice`);
       }
       const channel = new MessageChannel();
       const port = channel.port1;
@@ -425,9 +433,25 @@
         throw new Error(`Unknown language plugin method ${method}`);
       }
 
-      extensionServer.sendRequest(
-          {command: commands.RegisterLanguageExtensionPlugin, pluginName, port: channel.port2, supportedScriptTypes},
-          undefined, [channel.port2]);
+      await new Promise(resolve => {
+        extensionServer.sendRequest(
+            {command: commands.RegisterLanguageExtensionPlugin, pluginName, port: channel.port2, supportedScriptTypes},
+            () => resolve(), [channel.port2]);
+      });
+    },
+
+    /**
+     * @param {*} plugin The language plugin instance to unregister.
+     * @return {!Promise<void>}
+     */
+    unregisterLanguageExtensionPlugin: async function(plugin) {
+      const port = this._plugins.get(plugin);
+      if (!port) {
+        throw new Error('Tried to unregister a plugin that was not previously registered');
+      }
+      this._plugins.delete(plugin);
+      port.postMessage({event: languageExtensionPluginEvents.UnregisteredLanguageExtensionPlugin});
+      port.close();
     }
   };