Skip to content

Add WYSIWYG editor mode with formatting toolbar#1924

Closed
gjouret wants to merge 4 commits intoglushchenko:masterfrom
gjouret:WYSIWYG
Closed

Add WYSIWYG editor mode with formatting toolbar#1924
gjouret wants to merge 4 commits intoglushchenko:masterfrom
gjouret:WYSIWYG

Conversation

@gjouret
Copy link

@gjouret gjouret commented Mar 23, 2026

Summary

  • Adds a formatting toolbar below the title bar with buttons for bold, italic, underline, strikethrough, H1–H3, quote, bullet/numbered lists, checkbox, link, wiki-link, image/file insert, table, code block, and horizontal rule
  • Enables WYSIWYG mode by default — markdown syntax (#, **, *, ~~, `, [](), >) is hidden, showing only formatted text
  • Cmd+/ toggles between WYSIWYG and Source (raw markdown) mode, replacing the previous editor/preview toggle
  • Horizontal rules (---) render as visual separator lines
  • Blockquotes show a blue left border
  • Inline code backticks are hidden in WYSIWYG mode

Key fixes

  • Header ATX regex changed from .+? to .*? to match empty headers so # hides immediately
  • Removed erroneous +1 on header opening syntax range that was eating the first character of every header
  • LayoutManager.font(for:) now finds the largest font in a glyph range instead of using the first character's font, preventing line height collapse when lines start with hidden 0.1pt syntax characters

Files changed (11)

  • FSNotes/View/FormattingToolbar.swift — NEW: toolbar UI
  • FSNotes/EditorViewController.swift — toolbar setup, WYSIWYG/Source toggle rewrite
  • FSNotes/LayoutManager.swift — HR/blockquote drawing, line height fix
  • FSNotesCore/NotesTextProcessor.swift — regex fix, HR/blockquote/code-span hiding
  • FSNotesCore/TextFormatter.swift — numberedList(), horizontalRule(), insertTable()
  • FSNotesCore/UserDefaultsManagement.swift — wysiwygMode preference
  • FSNotes/View/EditTextView.swift — toolbar @IBAction methods, header behavior
  • FSNotesCore/Extensions/NSAttributedStringKey+.swift — .horizontalRule, .blockquote
  • FSNotes/ViewController.swift — toolbar wiring, hideSyntax init
  • FSNotes/NoteViewController.swift — toolbar wiring for separate windows

Test plan

  • Open a note with headers, bold, italic, links, code — syntax should be hidden
  • Cmd+/ toggles to Source mode (raw markdown visible) and back
  • Click H1/H2/H3 — # hidden, typing works, Return exits header
  • Click Bold/Italic/etc. — formatting applied to selection
  • Image button opens file picker
  • Wiki-link button inserts [[]] with autocomplete
  • --- on its own line renders as horizontal rule
  • > quote shows blue left border
  • No overlapping text on lines starting with hidden syntax

🤖 Generated with Claude Code

gjouret and others added 4 commits March 23, 2026 13:36
- Add FormattingToolbar with buttons for bold, italic, underline,
  strikethrough, H1-H3, quote, bullet/numbered lists, checkbox,
  link, wiki-link, image, table, code block, and horizontal rule
- All toolbar buttons route through first responder chain to
  EditTextView @IBAction methods (refusesFirstResponder keeps
  editor focused)
- Enable hideSyntax by default (wysiwygMode user preference)
- Cmd+/ toggles between WYSIWYG and Source mode instead of
  editor/WKWebView preview swap
- Fix header ATX regex to match empty headers (`.+?` -> `.*?`)
  so # syntax hides immediately when creating new headers
- Fix header opening syntax range — remove erroneous +1 that
  was eating the first content character of every header
- Fix LayoutManager.font(for:) to find largest font in glyph
  range instead of using first character's font, preventing
  line height collapse when lines start with hidden syntax
- Add horizontal rule rendering via LayoutManager custom drawing
- Add blockquote left border rendering via LayoutManager
- Hide inline code backticks in WYSIWYG mode
- Add custom NSAttributedString.Key: .horizontalRule, .blockquote
- Add TextFormatter methods: numberedList(), horizontalRule(),
  insertTable()
- Return key after header exits header mode (no # continuation)
- Wire FormattingToolbar into EditorViewController and
  NoteViewController view hierarchies

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Table Editor:
- Add TableEditorViewController with grid UI for creating/editing
  markdown tables (column/row steppers, header + data rows)
- Insert Table toolbar button opens popup sheet instead of
  inserting raw markdown template
- Parse existing tables and edit in-place

Blockquote fixes:
- Fix persistent blockquote bar: clear .blockquote and
  .horizontalRule attributes at start of each highlightMarkdown()
  pass so they only exist when regex matches
- Fix bar overlapping text: draw bar at fixed left margin position
  instead of relative to text bounding rect
- Fix blockquote > visibility: use clear foreground color with
  normal font size instead of 0.1pt hidden font, so cursor and
  layout work correctly while > is invisible
- Fix quote text color: normal color in WYSIWYG, gray in source
- Fix quote exit: rewrite matchChars() empty-line detection to
  strip prefix and check for empty content instead of fragile
  length arithmetic
- Blockquote bar and HR only render in WYSIWYG mode, not source

Other fixes:
- Fix underline: detect <u>...</u> tags in highlightMarkdown(),
  apply .underlineStyle attribute, hide tags in WYSIWYG mode
- Fix code block cursor: place at content area (+4) not language
  specifier (+3) to avoid accidental autocomplete
- Fix focus border: EditTextView.becomeFirstResponder() now calls
  showFocusBorder() so clicking in editor shows blue frame
- Fix wiki-link brackets: hide [[ and ]] via hideSyntaxIfNecessary
  using correct capture groups (1 and 3, not 0 and 2)
- Hide code block fence lines (```) in WYSIWYG mode via
  TextStorageProcessor

Navigation:
- Add Back/Forward buttons (chevron.left/right) to toolbar
- Track note navigation history in ViewController with
  browser-style back/forward stack (50 entries max)
- Buttons enable/disable based on available history

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Mermaid/code block rendering:
- Add BlockRenderer for inline mermaid diagram rendering via WKWebView snapshot
- Click-to-edit: click rendered diagram to restore source, edit, click away to re-render
- Retain BlockRenderer instances during async render to prevent use-after-free crash
- Fix WKWebView file access by writing temp HTML inside MPreview.bundle

Code review fixes (critical):
- Restore background dispatch in NotesTableView.reloadRow to prevent main-thread hangs
- Retain TableEditorViewController during sheet display to prevent stepper target crash
- Fix insertThumbnailCard race: capture note ref, verify identity in async callback
- Optimize LayoutManager.font(for:) from O(n) enumeration to O(1) lookup
- Change wysiwygMode default to false (users opt in)
- Revert unrelated defaults: noteContainer back to .none, useTextBundleToStoreDates to false

Code review fixes (important):
- Fix underline toggle: strip only outer <u></u> via string slicing, not global replace
- Fix navigateToHistoryNote flag race: clear isNavigatingHistory via async dispatch
- Make underline regex static on NotesTextProcessor (was recompiled every highlight call)
- Add updateLayer() to FormattingToolbar for dark/light mode color updates

AI chat panel:
- Add AIChatPanelView with streaming support and quick actions
- Add AIService for Anthropic/OpenAI API integration
- Add PDF export via PDFExporter

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the 0.1pt hidden font approach for hiding markdown syntax with
clear foreground color + negative kern. The 0.1pt font caused cascading
cursor bugs: invisible cursor after pressing H1/H2/H3, wrong cursor
position after Return on header lines, and miscalculated line heights.

The new approach preserves the existing font (so the cursor inherits
correct height) while making characters invisible via Color.clear and
collapsing their width via negative .kern per character.

Key changes:
- hideSyntaxIfNecessary() now uses clear color + negative kern instead
  of 0.1pt font, applied globally to all hidden syntax (**, *, ~~, `,
  # , [](), [[]], code fences, HR, mailto, autolinks)
- TextStorageProcessor.hideSyntaxRange() mirrors the same approach for
  code fence hiding
- LayoutManager.font(for:) enumerates to find the largest font when the
  first character is hidden, ensuring correct line fragment height for
  header lines
- Header Return key resets typingAttributes to body font before
  insertNewline so the new line doesn't inherit header styling
- applyHeader sets typingAttributes to header font for correct cursor
  height after pressing H1/H2/H3

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@iandol
Copy link

iandol commented Mar 24, 2026

I really don't want a visual markdown editor by default, will there be a way to disable this in the settings?

@gjouret
Copy link
Author

gjouret commented Mar 24, 2026

Yes. There will be a feature to start up FSNotes in either WYSIWYG mode or original markdown mode. Even in WYSIWYG mode, you can still type in markdown commands and the editor will accept them and instantly show you the result

@iandol
Copy link

iandol commented Mar 24, 2026

Also this may be confusing as now there are three views : (1) visual markdown editor (2) raw markdown editor (3) markdown view (not editor). Perhaps (1) and (3) should be merged if (1) can replace its functionality — but maybe things like math and others will not visualise in (1)?

@gjouret
Copy link
Author

gjouret commented Mar 24, 2026

there's only 1) WYSIWYG mode and ordinary 2) markdown mode. #2 is the current mode in FSNotes. In 1) the experience is more like Apple Notes or GitHub, where you have a toolbar to add formatting, etc. In #1, you can also type the mermaid commands directly (e.g. '## ' to create a new subheader), just keep typing, and it will format what you're typing as a subheader. You don't have to do this-but it's handy if you already know markdown well.

@gjouret
Copy link
Author

gjouret commented Mar 26, 2026

Closing - this work is for a private fork, not intended for upstream.

@gjouret gjouret closed this Mar 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants