How to Use Inline Edit with Word-Level Diff Preview in Obsidian Notes

Claudian provides an inline-edit workflow that renders word-level differences between original and AI-edited text directly inside the Obsidian editor, allowing you to accept or reject changes with single keystrokes.

The Claudian plugin transforms how you refine notes by combining Claude AI capabilities with a visual diff interface. This feature enables inline edit with word-level diff preview, letting you see exactly which words change before committing to any modification. Whether you are rewriting selected paragraphs or inserting new content at the cursor, the plugin highlights deletions and insertions using distinct CSS classes overlaid on the CodeMirror 6 editor.

Architecture of the Inline Edit System

The implementation spans multiple modules that handle UI state, LLM communication, and text diffing.

Core Components

Component Role Source Location
InlineEditModal Entry point that creates an InlineEditController and manages the lifecycle of an inline-edit session (open, accept, reject, cleanup). src/features/inline-edit/ui/InlineEditModal.ts
InlineEditController Renders the input widget, displays the diff preview, installs keyboard shortcuts, and calls InlineEditService to fetch edited text from the Claude model. Same file, lines 54–731
InlineEditService Sends user instructions plus context to the Claude SDK and returns the edited text wrapped in markup tags. Instantiated within the controller
Diff utilities (computeDiff / diffToHtml) Compute word-level differences and render them as HTML with <span class="claudian-diff-del"> (deletions) and <span class="claudian-diff-ins"> (insertions). src/features/inline-edit/ui/InlineEditModal.ts, lines 47–87 and 89–97
System prompt (inlineEdit.ts) Forces the LLM to wrap replacements in <replacement> tags and insertions in <insertion> tags, ensuring clean diff parsing. src/core/prompts/inlineEdit.ts
Utility helpers normalizeInsertionText trims blank lines from insertions; escapeHtml sanitizes output for safe rendering. src/utils/inlineEdit.ts
SelectionHighlight Visual block highlighting the selected text while the diff UI is active. src/shared/components/SelectionHighlight.ts

Session Flow

A complete inline-edit session follows this sequence:

  1. Trigger: You invoke the command via the Command Palette or hotkey (Ctrl+Shift+E).
  2. Initialization: InlineEditModal.openAndWait() instantiates an InlineEditController.
  3. Input Widget: The controller inserts an <input> element at the selection or cursor location using a CodeMirror 6 Decoration.widget.
  4. Instruction: You type a command (e.g., "translate to French" or "make this concise").
  5. LLM Request: On Enter, the controller calls InlineEditService.editText() with the instruction and context.
  6. Tagged Response: The Claude SDK returns text wrapped in <replacement> or <insertion> tags, enforced by the system prompt at src/core/prompts/inlineEdit.ts.
  7. Diff Computation: The controller extracts raw text and runs computeDiff(original, edited) to produce DiffOp objects.
  8. HTML Generation: diffToHtml() converts operations to HTML, escaping characters via escapeHtml from src/utils/inlineEdit.ts.
  9. Preview Render: A DiffWidget replaces the original text, displaying deletions in red (claudian-diff-del) and insertions in green (claudian-diff-ins).
  10. Decision: Press Enter to accept (write changes to file) or Esc to reject (restore original text).

Because computeDiff splits text using oldText.split(/(\s+)/), whitespace is preserved and consecutive changes merge for a clean preview.

Using the Inline Edit Command as an End-User

To leverage the inline edit with word-level diff preview feature:

  1. Select text in your note, or place the cursor where you want an insertion.
  2. Run "Claudian: Inline Edit" (default binding Ctrl+Shift+E).
  3. An inline input box appears at the selection/cursor location.
  4. Type your instruction:
    • Selection mode: translate to Spanish
    • Cursor mode: add a summary paragraph above
  5. Press Enter to submit. The word-level diff preview appears immediately, showing exactly which words are deleted or inserted.
  6. Press Enter again to accept the change, or Esc to reject and revert to the original text.

The diff preview ensures you verify the exact wording before the plugin writes changes to your markdown file.

Implementing Inline Edit Programmatically

You can trigger the inline-edit UI from your own Obsidian plugin code by instantiating InlineEditModal directly:

import { InlineEditModal } from '@/features/inline-edit/ui/InlineEditModal';
import type ClaudianPlugin from '../../main';
import type { App, Editor, MarkdownView } from 'obsidian';

async function startInlineEdit(app: App, plugin: ClaudianPlugin) {
  const view = app.workspace.getActiveViewOfType(MarkdownView);
  if (!view) return;

  const editor = view.editor;
  const sel = editor.getSelection();
  
  const editContext = sel
    ? { mode: 'selection' as const, selectedText: sel }
    : {
        mode: 'cursor' as const,
        cursorContext: {
          line: editor.getCursor('from').line,
          column: editor.getCursor('from').ch,
          isInbetween: false,
        },
      };

  const modal = new InlineEditModal(
    app,
    plugin,
    editor,
    view,
    editContext,
    view.file?.path ?? '',
  );

  const result = await modal.openAndWait();
  if (result.decision === 'accept') {
    console.log('User accepted edit:', result.editedText);
  } else {
    console.log('Edit was rejected or cancelled');
  }
}

The editContext parameter determines whether the modal operates in selection or cursor mode. The method returns a decision property indicating whether the user accepted or rejected the proposed changes.

Working with Diff Utilities

The plugin exposes utility functions for computing and rendering diffs that you can reuse in custom components.

Computing Word-Level Differences

import { computeDiff, diffToHtml } from '@/features/inline-edit/ui/InlineEditModal';

const oldText = 'hello world';
const newText = 'hello brave new world';

const ops = computeDiff(oldText, newText);
/* ops →
   [
     { type: 'equal',   text: 'hello ' },
     { type: 'insert',  text: 'brave new ' },
     { type: 'equal',   text: 'world' }
   ]
*/

const html = diffToHtml(ops);
// Result includes: <span class="claudian-diff-ins">brave new </span>

computeDiff and diffToHtml are defined in src/features/inline-edit/ui/InlineEditModal.ts (lines 47–87 and 89–97).

Normalizing Insertion Text

When the LLM returns insertions with leading blank lines, use normalizeInsertionText to clean them:

import { normalizeInsertionText } from '@/utils/inlineEdit';

const raw = '\n\n## Description\n\nThis project does X.\n\n';

const clean = normalizeInsertionText(raw);
// clean === '## Description\n\nThis project does X.'

This helper is located in src/utils/inlineEdit.ts (lines 10–12).

Escaping HTML for Safe Rendering

To prevent XSS when rendering user-generated or LLM-generated content:

import { escapeHtml } from '@/utils/inlineEdit';

const unsafe = '<script>alert("xss")</script>';
const safe = escapeHtml(unsafe);
// safe === '&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;'

The escapeHtml function is implemented in src/utils/inlineEdit.ts (lines 14–21).

Summary

  • Claudian implements inline edit with word-level diff preview through the InlineEditModal class in src/features/inline-edit/ui/InlineEditModal.ts.
  • The system uses computeDiff to compare texts word-by-word and diffToHtml to generate styled HTML using claudian-diff-del and claudian-diff-ins classes.
  • The workflow requires an editContext specifying either selection or cursor mode, submits instructions to InlineEditService, and renders results as an inline CodeMirror 6 widget.
  • Users accept changes with Enter or reject them with Esc, ensuring full control before modifying the underlying markdown file.
  • Helper utilities in src/utils/inlineEdit.ts handle text normalization and HTML escaping for secure rendering.

Frequently Asked Questions

How does the word-level diff algorithm preserve formatting?

The algorithm splits text using oldText.split(/(\s+)/), which captures whitespace as separate tokens. This ensures that spaces, newlines, and tabs remain intact during comparison. Consecutive operations of the same type (multiple insertions or deletions) are merged into single spans to produce a clean visual diff without fragmented highlighting.

Can I customize the keyboard shortcuts for accepting or rejecting edits?

Yes. The InlineEditController installs key mappings within the CodeMirror 6 editor instance. While the default bindings use Enter to accept and Esc to reject, you can modify the keymap configuration in src/features/inline-edit/ui/InlineEditModal.ts (within the controller logic) or override them via Obsidian's built-in hotkey settings if the plugin exposes configurable commands.

What happens if the LLM returns HTML characters or script tags in the edited text?

All LLM output passes through escapeHtml before rendering in the diff preview. This function converts characters like <, >, and & into their HTML entities (&lt;, &gt;, &amp;), preventing script injection and ensuring the word-level diff preview displays the literal text rather than executing it.

Is it possible to use inline edit without selecting text first?

Yes. The editContext supports a cursor mode that provides context around the cursor position (current line and column) rather than a specific selection. When you trigger the command without a selection, InlineEditService sends cursor context to the LLM, allowing you to insert new text or modify content based on surrounding paragraphs.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →