The TextFileView class extends EditableFileView (which extends FileView) and provides functionality for editing text files with automatic saving.
Properties
In-memory data representing the current content of the file.Since: 0.10.12
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)
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
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
The content to set in the view
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.
Since: 0.10.12
Methods
onUnloadFile()
Called when a file is unloaded from this view. Saves any pending changes.
onUnloadFile(file: TFile): 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>
Promise that resolves when file is loaded
Since: 0.10.12
save()
Saves the current content to the file.
save(clear?: boolean): Promise<void>
Whether to clear the dirty state after saving
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