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

> Master inline edit with word-level diff preview in Obsidian notes using Claudian. Accept or reject AI edits seamlessly within your editor for efficient content creation.

- Repository: [YishenTu/claudian](https://github.com/YishenTu/claudian)
- Tags: how-to-guide
- Published: 2026-03-17

---

**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`](https://github.com/YishenTu/claudian/blob/main/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`](https://github.com/YishenTu/claudian/blob/main/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`](https://github.com/YishenTu/claudian/blob/main/src/core/prompts/inlineEdit.ts) |
| **Utility helpers** | `normalizeInsertionText` trims blank lines from insertions; `escapeHtml` sanitizes output for safe rendering. | [`src/utils/inlineEdit.ts`](https://github.com/YishenTu/claudian/blob/main/src/utils/inlineEdit.ts) |
| **SelectionHighlight** | Visual block highlighting the selected text while the diff UI is active. | [`src/shared/components/SelectionHighlight.ts`](https://github.com/YishenTu/claudian/blob/main/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`](https://github.com/YishenTu/claudian/blob/main/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`](https://github.com/YishenTu/claudian/blob/main/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:

```typescript
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

```typescript
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`](https://github.com/YishenTu/claudian/blob/main/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:

```typescript
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`](https://github.com/YishenTu/claudian/blob/main/src/utils/inlineEdit.ts) (lines 10–12).

### Escaping HTML for Safe Rendering

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

```typescript
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`](https://github.com/YishenTu/claudian/blob/main/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`](https://github.com/YishenTu/claudian/blob/main/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`](https://github.com/YishenTu/claudian/blob/main/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`](https://github.com/YishenTu/claudian/blob/main/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.