xref: /llvm-project/mlir/utils/vscode/src/MLIR/bytecodeProvider.ts (revision 82b65496540fbee7720a4fa47ebc3c3dac128980)
1import * as base64 from 'base64-js'
2import * as vscode from 'vscode'
3
4import {MLIRContext} from '../mlirContext';
5
6/**
7 * The parameters to the mlir/convert(To|From)Bytecode commands. These
8 * parameters are:
9 * - `uri`: The URI of the file to convert.
10 */
11type ConvertBytecodeParams = Partial<{uri : string}>;
12
13/**
14 * The output of the mlir/convert(To|From)Bytecode commands:
15 * - `output`: The output buffer of the command, e.g. a .mlir or bytecode
16 *             buffer.
17 */
18type ConvertBytecodeResult = Partial<{output : string}>;
19
20/**
21 * A custom filesystem that is used to convert MLIR bytecode files to text for
22 * use in the editor, but still use bytecode on disk.
23 */
24class BytecodeFS implements vscode.FileSystemProvider {
25  mlirContext: MLIRContext;
26
27  constructor(mlirContext: MLIRContext) { this.mlirContext = mlirContext; }
28
29  /*
30   * Forward to the default filesystem for the various methods that don't need
31   * to understand the bytecode <-> text translation.
32   */
33  readDirectory(uri: vscode.Uri): Thenable<[ string, vscode.FileType ][]> {
34    return vscode.workspace.fs.readDirectory(uri);
35  }
36  delete(uri: vscode.Uri): void {
37    vscode.workspace.fs.delete(uri.with({scheme : "file"}));
38  }
39  stat(uri: vscode.Uri): Thenable<vscode.FileStat> {
40    return vscode.workspace.fs.stat(uri.with({scheme : "file"}));
41  }
42  rename(oldUri: vscode.Uri, newUri: vscode.Uri,
43         options: {overwrite: boolean}): void {
44    vscode.workspace.fs.rename(oldUri.with({scheme : "file"}),
45                               newUri.with({scheme : "file"}), options);
46  }
47  createDirectory(uri: vscode.Uri): void {
48    vscode.workspace.fs.createDirectory(uri.with({scheme : "file"}));
49  }
50  watch(_uri: vscode.Uri, _options: {
51    readonly recursive: boolean; readonly excludes : readonly string[]
52  }): vscode.Disposable {
53    return new vscode.Disposable(() => {});
54  }
55
56  private _emitter = new vscode.EventEmitter<vscode.FileChangeEvent[]>();
57  readonly onDidChangeFile: vscode.Event<vscode.FileChangeEvent[]> =
58      this._emitter.event;
59
60  /*
61   * Read in a bytecode file, converting it to text before returning it to the
62   * caller.
63   */
64  async readFile(uri: vscode.Uri): Promise<Uint8Array> {
65    // Try to start a language client for this file so that we can parse
66    // it.
67    const client =
68        await this.mlirContext.getOrActivateLanguageClient(uri, 'mlir');
69    if (!client) {
70      throw new Error(
71          'Failed to activate mlir language server to read bytecode');
72    }
73
74    // Ask the client to do the conversion.
75    let result: ConvertBytecodeResult;
76    try {
77      let params: ConvertBytecodeParams = {uri : uri.toString()};
78      result = await client.sendRequest('mlir/convertFromBytecode', params);
79    } catch (e) {
80      vscode.window.showErrorMessage(e.message);
81      throw new Error(`Failed to read bytecode file: ${e}`);
82    }
83    let resultBuffer = new TextEncoder().encode(result.output);
84
85    // NOTE: VSCode does not allow for extensions to manage files above 50mb.
86    // Detect that here and if our result is too large for us to manage, alert
87    // the user and open it as a new temporary .mlir file.
88    if (resultBuffer.length > (50 * 1024 * 1024)) {
89      const openAsTempInstead: vscode.MessageItem = {
90        title : 'Open as temporary .mlir instead',
91      };
92      const message: string = `Failed to open bytecode file "${
93          uri.toString()}". Cannot edit converted bytecode files larger than 50MB.`;
94      const errorResult: vscode.MessageItem|undefined =
95          await vscode.window.showErrorMessage(message, openAsTempInstead);
96      if (errorResult === openAsTempInstead) {
97        let tempFile = await vscode.workspace.openTextDocument({
98          language : 'mlir',
99          content : result.output,
100        });
101        await vscode.window.showTextDocument(tempFile);
102      }
103      throw new Error(message);
104    }
105
106    return resultBuffer;
107  }
108
109  /*
110   * Save the provided content, which contains MLIR text, as bytecode.
111   */
112  async writeFile(uri: vscode.Uri, content: Uint8Array,
113                  _options: {create: boolean, overwrite: boolean}) {
114    // Get the language client managing this file.
115    let client = this.mlirContext.getLanguageClient(uri, 'mlir');
116    if (!client) {
117      throw new Error(
118          'Failed to activate mlir language server to write bytecode');
119    }
120
121    // Ask the client to do the conversion.
122    let convertParams: ConvertBytecodeParams = {
123      uri : uri.toString(),
124    };
125    const result: ConvertBytecodeResult =
126        await client.sendRequest('mlir/convertToBytecode', convertParams);
127    await vscode.workspace.fs.writeFile(uri.with({scheme : "file"}),
128                                        base64.toByteArray(result.output));
129  }
130}
131
132/**
133 * A custom bytecode document for use by the custom editor provider below.
134 */
135class BytecodeDocument implements vscode.CustomDocument {
136  readonly uri: vscode.Uri;
137
138  constructor(uri: vscode.Uri) { this.uri = uri; }
139  dispose(): void {}
140}
141
142/**
143 * A custom editor provider for MLIR bytecode that allows for non-binary
144 * interpretation.
145 */
146class BytecodeEditorProvider implements
147    vscode.CustomReadonlyEditorProvider<BytecodeDocument> {
148  public async openCustomDocument(uri: vscode.Uri, _openContext: any,
149                                  _token: vscode.CancellationToken):
150      Promise<BytecodeDocument> {
151    return new BytecodeDocument(uri);
152  }
153
154  public async resolveCustomEditor(document: BytecodeDocument,
155                                   _webviewPanel: vscode.WebviewPanel,
156                                   _token: vscode.CancellationToken):
157      Promise<void> {
158    // Ask the user for the desired view type.
159    const editType = await vscode.window.showQuickPick(
160        [ {label : '.mlir', description : "Edit as a .mlir text file"} ],
161        {title : 'Select an editor for the bytecode.'},
162    );
163
164    // If we don't have a valid view type, just bail.
165    if (!editType) {
166      await vscode.commands.executeCommand(
167          'workbench.action.closeActiveEditor');
168      return;
169    }
170
171    // TODO: We should also provide a non-`.mlir` way of viewing the
172    // bytecode, which should also ideally have some support for invalid
173    // bytecode files.
174
175    // Close the active editor given that we aren't using it.
176    await vscode.commands.executeCommand('workbench.action.closeActiveEditor');
177
178    // Display the file using a .mlir format.
179    await vscode.window.showTextDocument(
180        document.uri.with({scheme : "mlir.bytecode-mlir"}),
181        {preview : true, preserveFocus : false});
182  }
183}
184
185/**
186 *  Register the necessary providers for supporting MLIR bytecode.
187 */
188export function registerMLIRBytecodeExtensions(context: vscode.ExtensionContext,
189                                               mlirContext: MLIRContext) {
190  vscode.workspace.registerFileSystemProvider("mlir.bytecode-mlir",
191                                              new BytecodeFS(mlirContext));
192  vscode.window.registerCustomEditorProvider('mlir.bytecode',
193                                             new BytecodeEditorProvider());
194}
195