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:
- Trigger: You invoke the command via the Command Palette or hotkey (
Ctrl+Shift+E). - Initialization:
InlineEditModal.openAndWait()instantiates anInlineEditController. - Input Widget: The controller inserts an
<input>element at the selection or cursor location using a CodeMirror 6Decoration.widget. - Instruction: You type a command (e.g., "translate to French" or "make this concise").
- LLM Request: On Enter, the controller calls
InlineEditService.editText()with the instruction and context. - Tagged Response: The Claude SDK returns text wrapped in
<replacement>or<insertion>tags, enforced by the system prompt atsrc/core/prompts/inlineEdit.ts. - Diff Computation: The controller extracts raw text and runs
computeDiff(original, edited)to produceDiffOpobjects. - HTML Generation:
diffToHtml()converts operations to HTML, escaping characters viaescapeHtmlfromsrc/utils/inlineEdit.ts. - Preview Render: A
DiffWidgetreplaces the original text, displaying deletions in red (claudian-diff-del) and insertions in green (claudian-diff-ins). - 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:
- Select text in your note, or place the cursor where you want an insertion.
- Run "Claudian: Inline Edit" (default binding
Ctrl+Shift+E). - An inline input box appears at the selection/cursor location.
- Type your instruction:
- Selection mode:
translate to Spanish - Cursor mode:
add a summary paragraph above
- Selection mode:
- Press Enter to submit. The word-level diff preview appears immediately, showing exactly which words are deleted or inserted.
- 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 === '<script>alert("xss")</script>'
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
InlineEditModalclass insrc/features/inline-edit/ui/InlineEditModal.ts. - The system uses
computeDiffto compare texts word-by-word anddiffToHtmlto generate styled HTML usingclaudian-diff-delandclaudian-diff-insclasses. - The workflow requires an
editContextspecifying either selection or cursor mode, submits instructions toInlineEditService, 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.tshandle 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 (<, >, &), 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →