Skip to main content
The TextFileView class extends EditableFileView (which extends FileView) and provides functionality for editing text files with automatic saving.

Properties

data
string
In-memory data representing the current content of the file.Since: 0.10.12
requestSave
() => void
Debounced save function that saves the file 2 seconds after being called.Since: 0.10.12

Inherited Properties

TextFileView inherits properties from FileView, ItemView, and View:
  • file: TFile | null
  • allowNoFile: boolean
  • contentEl: HTMLElement
  • app: App
  • leaf: WorkspaceLeaf
  • containerEl: HTMLElement

Constructor

constructor(leaf: WorkspaceLeaf)
leaf
WorkspaceLeaf
required
The workspace leaf this view will be attached to

Abstract Methods

These methods must be implemented by any class extending TextFileView:

getViewData()

Gets the data from the editor. This will be called to save the editor contents to the file.
abstract getViewData(): string
return
string
The current content of the view to be saved
Since: 0.10.12

setViewData()

Sets the data to the editor. This is used to load the file contents.
abstract setViewData(data: string, clear: boolean): void
data
string
required
The content to set in the view
clear
boolean
required
If true, this is opening a completely different file. You should call clear() or implement an efficient clearing mechanism given the new data.
Since: 0.10.12

clear()

Clears the editor. This is usually called when opening a completely different file.
It’s best to clear editor states like undo-redo history and any caches/indexes associated with the previous file contents.
abstract clear(): void
Since: 0.10.12

Methods

onUnloadFile()

Called when a file is unloaded from this view. Saves any pending changes.
onUnloadFile(file: TFile): Promise<void>
file
TFile
required
The file being unloaded
return
Promise<void>
Promise that resolves when file is unloaded
Since: 0.10.12

onLoadFile()

Called when a file is loaded into this view. Reads the file and calls setViewData().
onLoadFile(file: TFile): Promise<void>
file
TFile
required
The file being loaded
return
Promise<void>
Promise that resolves when file is loaded
Since: 0.10.12

save()

Saves the current content to the file.
save(clear?: boolean): Promise<void>
clear
boolean
Whether to clear the dirty state after saving
return
Promise<void>
Promise that resolves when save is complete
Since: 0.10.12

Creating a Custom TextFileView

Here’s an example of creating a custom text file editor:
import { TextFileView, TFile, WorkspaceLeaf } from 'obsidian';

const VIEW_TYPE_CUSTOM = 'custom-text-editor';

class CustomTextEditor extends TextFileView {
  private textArea: HTMLTextAreaElement;

  constructor(leaf: WorkspaceLeaf) {
    super(leaf);
  }

  getViewType() {
    return VIEW_TYPE_CUSTOM;
  }

  getDisplayText() {
    return this.file?.basename ?? 'Custom Editor';
  }

  async onOpen() {
    const container = this.contentEl;
    container.empty();

    // Create textarea for editing
    this.textArea = container.createEl('textarea', {
      cls: 'custom-editor-textarea'
    });

    // Listen for changes and request save
    this.textArea.addEventListener('input', () => {
      this.requestSave();
    });
  }

  getViewData(): string {
    return this.textArea?.value ?? '';
  }

  setViewData(data: string, clear: boolean): void {
    if (clear) {
      this.clear();
    }
    
    if (this.textArea) {
      this.textArea.value = data;
      this.data = data;
    }
  }

  clear(): void {
    if (this.textArea) {
      this.textArea.value = '';
    }
    this.data = '';
  }

  canAcceptExtension(extension: string): boolean {
    return extension === 'txt' || extension === 'md';
  }

  async onClose() {
    // Cleanup
    this.textArea = null;
  }
}

Advanced Example: Code Editor

Here’s a more advanced example with syntax highlighting:
import { TextFileView, WorkspaceLeaf } from 'obsidian';

const VIEW_TYPE_CODE = 'code-editor';

class CodeEditorView extends TextFileView {
  private editor: HTMLElement;
  private lineNumbers: HTMLElement;

  constructor(leaf: WorkspaceLeaf) {
    super(leaf);
  }

  getViewType() {
    return VIEW_TYPE_CODE;
  }

  getDisplayText() {
    return this.file?.basename ?? 'Code Editor';
  }

  async onOpen() {
    const container = this.contentEl;
    container.empty();
    container.addClass('code-editor-container');

    // Create editor layout
    const editorWrapper = container.createDiv('editor-wrapper');
    this.lineNumbers = editorWrapper.createDiv('line-numbers');
    this.editor = editorWrapper.createDiv('editor');
    this.editor.contentEditable = 'true';

    // Listen for changes
    this.editor.addEventListener('input', () => {
      this.updateLineNumbers();
      this.requestSave();
    });

    this.editor.addEventListener('keydown', (e) => {
      if (e.key === 'Tab') {
        e.preventDefault();
        document.execCommand('insertText', false, '  ');
      }
    });

    // Add action buttons
    this.addAction('copy', 'Copy all', () => {
      navigator.clipboard.writeText(this.getViewData());
    });

    this.addAction('rotate-cw', 'Format', () => {
      this.formatCode();
    });
  }

  getViewData(): string {
    return this.editor?.innerText ?? '';
  }

  setViewData(data: string, clear: boolean): void {
    if (clear) {
      this.clear();
    }

    if (this.editor) {
      this.editor.innerText = data;
      this.data = data;
      this.updateLineNumbers();
    }
  }

  clear(): void {
    if (this.editor) {
      this.editor.innerText = '';
    }
    if (this.lineNumbers) {
      this.lineNumbers.empty();
    }
    this.data = '';
  }

  updateLineNumbers() {
    if (!this.lineNumbers || !this.editor) return;

    const lines = this.editor.innerText.split('\n').length;
    this.lineNumbers.empty();

    for (let i = 1; i <= lines; i++) {
      this.lineNumbers.createDiv('line-number', { text: i.toString() });
    }
  }

  formatCode() {
    const data = this.getViewData();
    // Apply formatting logic
    const formatted = data.split('\n').map(line => line.trim()).join('\n');
    this.setViewData(formatted, false);
    this.requestSave();
  }

  canAcceptExtension(extension: string): boolean {
    return ['js', 'ts', 'json', 'css', 'html'].includes(extension);
  }

  getState() {
    const state = super.getState();
    state.scrollTop = this.editor?.scrollTop ?? 0;
    return state;
  }

  async setState(state: any, result: ViewStateResult) {
    await super.setState(state, result);
    if (state.scrollTop && this.editor) {
      this.editor.scrollTop = state.scrollTop;
    }
  }
}

Registering and Using the View

import { Plugin } from 'obsidian';

export default class CustomEditorPlugin extends Plugin {
  async onload() {
    // Register the view
    this.registerView(
      VIEW_TYPE_CUSTOM,
      (leaf) => new CustomTextEditor(leaf)
    );

    // Register for specific file extensions
    this.registerExtensions(['txt'], VIEW_TYPE_CUSTOM);

    // Add command to open in custom editor
    this.addCommand({
      id: 'open-in-custom-editor',
      name: 'Open in custom editor',
      callback: () => {
        const file = this.app.workspace.getActiveFile();
        if (file) {
          const leaf = this.app.workspace.getLeaf(false);
          leaf.setViewState({
            type: VIEW_TYPE_CUSTOM,
            state: { file: file.path }
          });
        }
      }
    });
  }

  async onunload() {
    this.app.workspace.detachLeavesOfType(VIEW_TYPE_CUSTOM);
  }
}

Automatic Saving

TextFileView provides automatic saving through the requestSave() method:
class MyEditor extends TextFileView {
  setupEditor() {
    this.editor.addEventListener('input', () => {
      // This will save 2 seconds after the last edit
      this.requestSave();
    });
  }

  // To save immediately
  async saveNow() {
    await this.save();
  }
}

Best Practices

1. Handle the clear parameter properly

setViewData(data: string, clear: boolean): void {
  if (clear) {
    // Clear undo history, caches, etc.
    this.clear();
  }
  
  // Set the new data
  this.editor.value = data;
}

2. Debounce expensive operations

class MyEditor extends TextFileView {
  private updateTimeout: number;

  onEditorChange() {
    // Request save (already debounced)
    this.requestSave();

    // Debounce expensive updates
    if (this.updateTimeout) {
      window.clearTimeout(this.updateTimeout);
    }
    
    this.updateTimeout = window.setTimeout(() => {
      this.updatePreview();
    }, 500);
  }
}

3. Clean up in onClose()

async onClose() {
  // Clear any timeouts
  if (this.updateTimeout) {
    window.clearTimeout(this.updateTimeout);
  }
  
  // Clear references
  this.editor = null;
  
  await super.onClose();
}

4. Handle large files efficiently

setViewData(data: string, clear: boolean): void {
  if (clear) {
    this.clear();
  }

  // For very large files, consider virtual scrolling
  // or loading in chunks
  if (data.length > 1000000) {
    this.loadInChunks(data);
  } else {
    this.editor.value = data;
  }
}

See Also

Build docs developers (and LLMs) love