All files / src customTextEditorProvider.ts

98.11% Statements 52/53
100% Branches 27/27
92.3% Functions 12/13
100% Lines 49/49

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102            27x   27x     4x 2x 1x 1x                   16x 16x   16x   16x 16x   16x   16x 7x 7x   6x 6x               16x 6x 6x 6x     16x 4x 4x 2x   4x     16x 4x       4x 1x 1x     3x 3x 1x       16x 4x 3x       16x 11x 4x 7x 2x 5x 4x   5x     16x 1x 1x 1x        
import * as vscode from 'vscode';
import { inferFileType } from './editorTypes';
import { applyTextDocumentContent } from './textDocumentEdits';
import { configureEditorWebview, getResourceBaseUri } from './webviewHost';
 
export class VibeCustomTextEditorProvider implements vscode.CustomTextEditorProvider {
  private readonly panels = new Set<vscode.WebviewPanel>();
 
  constructor(private readonly context: vscode.ExtensionContext) {}
 
  toggleMode() {
    for (const panel of this.panels) {
      if (panel.active) {
        panel.webview.postMessage({ type: 'toggleMode' });
        return;
      }
    }
  }
 
  async resolveCustomTextEditor(
    document: vscode.TextDocument,
    panel: vscode.WebviewPanel,
    _token: vscode.CancellationToken,
  ): Promise<void> {
    const resourceKey = document.uri.toString();
    const fileType = inferFileType(document.uri.fsPath);
    let lastSentContent: string | undefined;
    let webviewMessageQueue: Promise<unknown> = Promise.resolve();
 
    this.panels.add(panel);
    panel.iconPath = new vscode.ThemeIcon('open-preview');
 
    configureEditorWebview(this.context, panel, document.uri);
 
    const postDocumentContent = (force = false) => {
      const content = document.getText();
      if (!force && content === lastSentContent) return;
 
      lastSentContent = content;
      panel.webview.postMessage({
        type: 'update',
        content,
        baseUri: getResourceBaseUri(panel, document.uri),
        fileType,
      });
    };
 
    const enqueueWebviewMessage = <T>(task: () => Promise<T>) => {
      const run = webviewMessageQueue.then(task, task);
      webviewMessageQueue = run.catch(() => undefined);
      return run;
    };
 
    const applyContentFromWebview = async (content: string) => {
      const applied = await applyTextDocumentContent(document, content);
      if (applied) {
        lastSentContent = content;
      }
      return applied;
    };
 
    const saveDocument = async (content?: string) => {
      const applied = typeof content === 'string'
        ? await applyContentFromWebview(content)
        : true;
 
      if (!applied) {
        vscode.window.showErrorMessage(`Failed to apply changes before saving ${document.fileName}.`);
        return;
      }
 
      const saved = await document.save();
      if (!saved) {
        vscode.window.showErrorMessage(`Failed to save ${document.fileName}.`);
      }
    };
 
    const changeListener = vscode.workspace.onDidChangeTextDocument(event => {
      if (event.document.uri.toString() === resourceKey) {
        postDocumentContent();
      }
    });
 
    const messageListener = panel.webview.onDidReceiveMessage(message => {
      if (message.type === 'ready') {
        postDocumentContent(true);
      } else if (message.type === 'edit' && typeof message.content === 'string') {
        return enqueueWebviewMessage(() => applyContentFromWebview(message.content));
      } else if (message.type === 'save') {
        return enqueueWebviewMessage(() => saveDocument(typeof message.content === 'string' ? message.content : undefined));
      }
      return undefined;
    });
 
    panel.onDidDispose(() => {
      this.panels.delete(panel);
      changeListener.dispose();
      messageListener.dispose();
    });
  }
}