Move WasmDisassembly to TextUtils and make it a ContentData subclass

We don't want a dependency of 'core/common' to 'models/text_utils'.

Mostly a mechanical change, the interesting bit is probably the
`super` call in the WasmDisassembly constructor and an overridden
`asDeferedContent`.

R=pfaffe@chromium.org

Bug: 342529015
Change-Id: I3d42bdeed17d40257e4b722cbd498dc6b09215a3
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/5569449
Commit-Queue: Simon Zünd <szuend@chromium.org>
Reviewed-by: Philip Pfaffe <pfaffe@chromium.org>
diff --git a/config/gni/devtools_grd_files.gni b/config/gni/devtools_grd_files.gni
index 25ba0bf..f5f93b8 100644
--- a/config/gni/devtools_grd_files.gni
+++ b/config/gni/devtools_grd_files.gni
@@ -713,7 +713,6 @@
   "front_end/core/common/TextDictionary.js",
   "front_end/core/common/Throttler.js",
   "front_end/core/common/Trie.js",
-  "front_end/core/common/WasmDisassembly.js",
   "front_end/core/common/Worker.js",
   "front_end/core/dom_extension/DOMExtension.js",
   "front_end/core/host/AidaClient.js",
@@ -959,6 +958,7 @@
   "front_end/models/text_utils/TextCursor.js",
   "front_end/models/text_utils/TextRange.js",
   "front_end/models/text_utils/TextUtils.js",
+  "front_end/models/text_utils/WasmDisassembly.js",
   "front_end/models/timeline_model/TimelineJSProfile.js",
   "front_end/models/timeline_model/TimelineModelFilter.js",
   "front_end/models/timeline_model/TimelineProfileTree.js",
diff --git a/front_end/core/common/BUILD.gn b/front_end/core/common/BUILD.gn
index 5cf2fdf..da6357b 100644
--- a/front_end/core/common/BUILD.gn
+++ b/front_end/core/common/BUILD.gn
@@ -41,7 +41,6 @@
     "TextDictionary.ts",
     "Throttler.ts",
     "Trie.ts",
-    "WasmDisassembly.ts",
     "Worker.ts",
   ]
 
@@ -99,7 +98,6 @@
     "TextDictionary.test.ts",
     "Throttler.test.ts",
     "Trie.test.ts",
-    "WasmDisassembly.test.ts",
   ]
 
   deps = [
diff --git a/front_end/core/common/common.ts b/front_end/core/common/common.ts
index 4b616d4..81b49f1 100644
--- a/front_end/core/common/common.ts
+++ b/front_end/core/common/common.ts
@@ -32,7 +32,6 @@
 import * as TextDictionary from './TextDictionary.js';
 import * as Throttler from './Throttler.js';
 import * as Trie from './Trie.js';
-import * as WasmDisassembly from './WasmDisassembly.js';
 import * as Worker from './Worker.js';
 
 /*
@@ -74,5 +73,4 @@
   Throttler,
   Trie,
   Worker,
-  WasmDisassembly,
 };
diff --git a/front_end/core/sdk/Script.ts b/front_end/core/sdk/Script.ts
index e6572cb..493fa41 100644
--- a/front_end/core/sdk/Script.ts
+++ b/front_end/core/sdk/Script.ts
@@ -251,7 +251,7 @@
     for (let i = 0; i < functionBodyOffsets.length; i += 2) {
       functionBodyRanges.push({start: functionBodyOffsets[i], end: functionBodyOffsets[i + 1]});
     }
-    const wasmDisassemblyInfo = new Common.WasmDisassembly.WasmDisassembly(
+    const wasmDisassemblyInfo = new TextUtils.WasmDisassembly.WasmDisassembly(
         lines.concat(...lineChunks), bytecodeOffsets.concat(...bytecodeOffsetChunks), functionBodyRanges);
     return {content: '', isEncoded: false, wasmDisassemblyInfo};
   }
@@ -488,10 +488,10 @@
 
 export const sourceURLRegex = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/;
 
-async function disassembleWasm(content: string): Promise<Common.WasmDisassembly.WasmDisassembly> {
+async function disassembleWasm(content: string): Promise<TextUtils.WasmDisassembly.WasmDisassembly> {
   const worker = Common.Worker.WorkerWrapper.fromURL(
       new URL('../../entrypoints/wasmparser_worker/wasmparser_worker-entrypoint.js', import.meta.url));
-  const promise = new Promise<Common.WasmDisassembly.WasmDisassembly>((resolve, reject) => {
+  const promise = new Promise<TextUtils.WasmDisassembly.WasmDisassembly>((resolve, reject) => {
     worker.onmessage = ({data}: MessageEvent) => {
       if ('method' in data) {
         switch (data.method) {
@@ -500,7 +500,7 @@
               reject(data.error);
             } else if ('result' in data) {
               const {lines, offsets, functionBodyOffsets} = data.result;
-              resolve(new Common.WasmDisassembly.WasmDisassembly(lines, offsets, functionBodyOffsets));
+              resolve(new TextUtils.WasmDisassembly.WasmDisassembly(lines, offsets, functionBodyOffsets));
             }
             break;
         }
diff --git a/front_end/models/text_utils/BUILD.gn b/front_end/models/text_utils/BUILD.gn
index 1d1923e..7eb12d5 100644
--- a/front_end/models/text_utils/BUILD.gn
+++ b/front_end/models/text_utils/BUILD.gn
@@ -18,6 +18,7 @@
     "TextCursor.ts",
     "TextRange.ts",
     "TextUtils.ts",
+    "WasmDisassembly.ts",
   ]
 
   deps = [
@@ -62,6 +63,7 @@
     "TextCursor.test.ts",
     "TextRange.test.ts",
     "TextUtils.test.ts",
+    "WasmDisassembly.test.ts",
   ]
 
   deps = [
diff --git a/front_end/models/text_utils/ContentProvider.ts b/front_end/models/text_utils/ContentProvider.ts
index fba65b1..bcf1f3c 100644
--- a/front_end/models/text_utils/ContentProvider.ts
+++ b/front_end/models/text_utils/ContentProvider.ts
@@ -33,6 +33,7 @@
 
 import {type ContentDataOrError} from './ContentData.js';
 import {type StreamingContentDataOrError} from './StreamingContentData.js';
+import {type WasmDisassembly} from './WasmDisassembly.js';
 
 export interface ContentProvider {
   contentURL(): Platform.DevToolsPath.UrlString;
@@ -71,7 +72,7 @@
 }|{
   content: '',
   isEncoded: false,
-  wasmDisassemblyInfo: Common.WasmDisassembly.WasmDisassembly,
+  wasmDisassemblyInfo: WasmDisassembly,
 }|{
   content: null,
   error: string,
diff --git a/front_end/core/common/WasmDisassembly.test.ts b/front_end/models/text_utils/WasmDisassembly.test.ts
similarity index 64%
rename from front_end/core/common/WasmDisassembly.test.ts
rename to front_end/models/text_utils/WasmDisassembly.test.ts
index 5d146f7..4dc2a7d 100644
--- a/front_end/core/common/WasmDisassembly.test.ts
+++ b/front_end/models/text_utils/WasmDisassembly.test.ts
@@ -2,9 +2,9 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import * as Common from './common.js';
+import * as TextUtils from './text_utils.js';
 
-const WasmDisassembly = Common.WasmDisassembly.WasmDisassembly;
+const WasmDisassembly = TextUtils.WasmDisassembly.WasmDisassembly;
 
 describe('WasmDisassembly', () => {
   const LINES = ['A', 'B', 'C', 'D', 'E', 'F', 'G', ' H', 'I'];
@@ -34,4 +34,23 @@
     const disassembly = new WasmDisassembly(LINES, BYTECODE_OFFSETS, FUNCTION_BODY_OFFSETS);
     assert.deepEqual([...disassembly.nonBreakableLineNumbers()], [0, 1, 2, 3, 8]);
   });
+
+  it('can be converted to a DeferredContent', () => {
+    const disassembly = new WasmDisassembly(LINES, BYTECODE_OFFSETS, FUNCTION_BODY_OFFSETS);
+    const content = disassembly.asDeferedContent();
+
+    if ('wasmDisassemblyInfo' in content) {
+      assert.strictEqual(content.wasmDisassemblyInfo, disassembly);
+    } else {
+      assert.fail('wasmDissasembly not set on DeferredContent');
+    }
+    assert.isEmpty(content.content);
+    assert.isFalse(content.isEncoded);
+  });
+
+  it('produces the joined lines for the "text" property', () => {
+    const disassembly = new WasmDisassembly(LINES, BYTECODE_OFFSETS, FUNCTION_BODY_OFFSETS);
+
+    assert.strictEqual(disassembly.text, 'A\nB\nC\nD\nE\nF\nG\n H\nI');
+  });
 });
diff --git a/front_end/core/common/WasmDisassembly.ts b/front_end/models/text_utils/WasmDisassembly.ts
similarity index 61%
rename from front_end/core/common/WasmDisassembly.ts
rename to front_end/models/text_utils/WasmDisassembly.ts
index f05becd..4eb6748 100644
--- a/front_end/core/common/WasmDisassembly.ts
+++ b/front_end/models/text_utils/WasmDisassembly.ts
@@ -1,24 +1,31 @@
-// Copyright 2020 The Chromium Authors. All rights reserved.
+// Copyright 2024 The Chromium Authors. All rights reserved.
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import * as Platform from '../platform/platform.js';
+import * as Platform from '../../core/platform/platform.js';
 
-/**
- * Metadata to map between bytecode #offsets and line numbers in the
- * disassembly for WebAssembly modules.
- */
+import {ContentData} from './ContentData.js';
+import { type DeferredContent } from './ContentProvider.js';
 
 interface FunctionBodyOffset {
   start: number;
   end: number;
 }
-export class WasmDisassembly {
+
+/**
+ * Metadata to map between bytecode #offsets and line numbers in the
+ * disassembly for WebAssembly modules.
+ */
+export class WasmDisassembly extends ContentData {
   readonly lines: string[];
   readonly #offsets: number[];
   #functionBodyOffsets: FunctionBodyOffset[];
 
+  // Wasm can be potentially very large, so we calculate `text' lazily.
+  #cachedText?: string;
+
   constructor(lines: string[], offsets: number[], functionBodyOffsets: FunctionBodyOffset[]) {
+    super('', /* isBase64 */ false, 'text/x-wast', 'utf-8');
     if (lines.length !== offsets.length) {
       throw new Error('Lines and offsets don\'t match');
     }
@@ -27,6 +34,18 @@
     this.#functionBodyOffsets = functionBodyOffsets;
   }
 
+  override get text(): string {
+    if (typeof this.#cachedText === 'undefined') {
+      this.#cachedText = this.lines.join('\n');
+    }
+    return this.#cachedText;
+  }
+
+  override get isEmpty(): boolean {
+    // Don't trigger unnecessary concatenating. Only check whether we have no lines, or a single empty line.
+    return this.lines.length === 0 || (this.lines.length === 1 && this.lines[0].length === 0);
+  }
+
   get lineNumbers(): number {
     return this.#offsets.length;
   }
@@ -58,4 +77,11 @@
       yield lineNumber++;
     }
   }
+
+  /**
+   * @deprecated Used during migration from `DeferredContent` to `ContentData`.
+   */
+  override asDeferedContent(): DeferredContent {
+    return {content: '', isEncoded: false, wasmDisassemblyInfo: this};
+  }
 }
diff --git a/front_end/models/text_utils/text_utils.ts b/front_end/models/text_utils/text_utils.ts
index 83d23f0..c1cdb1d 100644
--- a/front_end/models/text_utils/text_utils.ts
+++ b/front_end/models/text_utils/text_utils.ts
@@ -11,6 +11,7 @@
 import * as TextCursor from './TextCursor.js';
 import * as TextRange from './TextRange.js';
 import * as TextUtils from './TextUtils.js';
+import * as WasmDisassembly from './WasmDisassembly.js';
 
 export {
   CodeMirrorUtils,
@@ -22,4 +23,5 @@
   TextCursor,
   TextRange,
   TextUtils,
+  WasmDisassembly,
 };
diff --git a/front_end/models/workspace/UISourceCode.ts b/front_end/models/workspace/UISourceCode.ts
index cfd3326..609d404 100644
--- a/front_end/models/workspace/UISourceCode.ts
+++ b/front_end/models/workspace/UISourceCode.ts
@@ -226,8 +226,11 @@
     }
 
     if (cachedWasmOnly && this.mimeType() === 'application/wasm') {
-      return Promise.resolve(
-          {content: '', isEncoded: false, wasmDisassemblyInfo: new Common.WasmDisassembly.WasmDisassembly([], [], [])});
+      return Promise.resolve({
+        content: '',
+        isEncoded: false,
+        wasmDisassemblyInfo: new TextUtils.WasmDisassembly.WasmDisassembly([], [], []),
+      });
     }
 
     this.requestContentPromise = this.requestContentImpl();
diff --git a/front_end/panels/sources/components/BreakpointsView.ts b/front_end/panels/sources/components/BreakpointsView.ts
index bd885c0..78fe542 100644
--- a/front_end/panels/sources/components/BreakpointsView.ts
+++ b/front_end/panels/sources/components/BreakpointsView.ts
@@ -507,7 +507,7 @@
   }
 
   #getContent(locations: Breakpoints.BreakpointManager.BreakpointLocation[][]):
-      Promise<Array<TextUtils.Text.Text|Common.WasmDisassembly.WasmDisassembly>> {
+      Promise<Array<TextUtils.Text.Text|TextUtils.WasmDisassembly.WasmDisassembly>> {
     // Use a cache to share the Text objects between all breakpoints. This way
     // we share the cached line ending information that Text calculates. This
     // was very slow to calculate with a lot of breakpoints in the same very
diff --git a/front_end/ui/legacy/components/source_frame/SourceFrame.ts b/front_end/ui/legacy/components/source_frame/SourceFrame.ts
index 978e08d..00ba299 100644
--- a/front_end/ui/legacy/components/source_frame/SourceFrame.ts
+++ b/front_end/ui/legacy/components/source_frame/SourceFrame.ts
@@ -176,7 +176,7 @@
   private selectionToSet: TextUtils.TextRange.TextRange|null;
   private loadedInternal: boolean;
   private contentRequested: boolean;
-  private wasmDisassemblyInternal: Common.WasmDisassembly.WasmDisassembly|null;
+  private wasmDisassemblyInternal: TextUtils.WasmDisassembly.WasmDisassembly|null;
   contentSet: boolean;
   private selfXssWarningDisabledSetting: Common.Settings.Setting<boolean>;
 
@@ -348,7 +348,7 @@
     }
   }
 
-  get wasmDisassembly(): Common.WasmDisassembly.WasmDisassembly|null {
+  get wasmDisassembly(): TextUtils.WasmDisassembly.WasmDisassembly|null {
     return this.wasmDisassemblyInternal;
   }
 
@@ -1252,7 +1252,7 @@
   return !found;
 }
 
-function markNonBreakableLines(disassembly: Common.WasmDisassembly.WasmDisassembly): CodeMirror.Extension {
+function markNonBreakableLines(disassembly: TextUtils.WasmDisassembly.WasmDisassembly): CodeMirror.Extension {
   // Mark non-breakable lines in the Wasm disassembly after setting
   // up the content for the text editor (which creates the gutter).
   return nonBreakableLines.init(state => {