fix: auto-focus rich markdown editor on mount (#750)

* fix: auto-focus rich markdown editor on mount

Match MonacoEditor's behavior so opening a new markdown file (Cmd+Shift+N)
or switching to a rich-markdown tab lands the cursor in the editor. Guards
against focus theft from modals/dialogs and passes `scrollIntoView: false`
so the focus call doesn't race with useEditorScrollRestore's RAF retry loop.

* fix: use 'start' position to avoid AllSelection on empty editor

Passing null to commands.focus() keeps the editor's current selection,
which for a fresh empty document is an AllSelection — rendered as a
visible 0-width highlight inside the placeholder instead of a normal
caret. 'start' resolves to a proper TextSelection at doc start.

Also update review-and-submit skill to push before gh pr create.
This commit is contained in:
Jinjing 2026-04-16 23:30:14 -07:00 committed by GitHub
parent d1a3324e7e
commit fcbdd95b43
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 53 additions and 0 deletions

View File

@ -133,6 +133,17 @@ fi
### 2a. Create PR
**FIRST**: Push the branch to the remote so `gh pr create` doesn't fail with
`aborted: you must first push the current branch to a remote`. The
`create-pr` skill rebases locally but does not always push before invoking
`gh pr create`, and `gh` refuses to create a PR for an un-pushed branch.
```bash
git push --force-with-lease -u origin HEAD
```
Then invoke:
```
Use the Skill tool: skill: "create-pr"
```

View File

@ -24,6 +24,7 @@ import { createRichMarkdownKeyHandler } from './rich-markdown-key-handler'
import { DOMSerializer } from '@tiptap/pm/model'
import { TextSelection } from '@tiptap/pm/state'
import { normalizeSoftBreaks } from './rich-markdown-normalize'
import { autoFocusRichEditor } from './rich-markdown-auto-focus'
import { cutVisualLine, getVisualLineRange } from './rich-markdown-visual-line'
type RichMarkdownEditorProps = {
@ -270,6 +271,12 @@ export default function RichMarkdownEditor({
// Why: clear the flag *after* normalizeSoftBreaks so any onUpdate
// triggered by the normalization transaction is still suppressed.
isInitializingRef.current = false
// Why: MonacoEditor already auto-focuses on mount so users can start
// typing immediately. The rich markdown editor must do the same,
// otherwise opening a new markdown file (Cmd+Shift+N) or switching to
// an existing markdown tab leaves the cursor outside the editing
// surface and the user has to click before typing.
autoFocusRichEditor(nextEditor, rootRef.current)
},
onUpdate: ({ editor: nextEditor }) => {
syncSlashMenu(nextEditor, rootRef.current, setSlashMenu)

View File

@ -0,0 +1,35 @@
import type { Editor } from '@tiptap/react'
/**
* Auto-focuses the rich markdown editor on mount so users can start typing
* immediately (matching MonacoEditor's behavior). Guards against focus theft
* from modals/dialogs and skips scrollIntoView to avoid racing with
* useEditorScrollRestore.
*/
export function autoFocusRichEditor(nextEditor: Editor, rootEl: HTMLElement | null): void {
requestAnimationFrame(() => {
if (nextEditor.isDestroyed) {
return
}
// Why: don't steal focus if something outside the editor root is already
// focused (modal, rename dialog, sidebar search input, etc.). Only
// auto-focus when focus is nowhere or already inside the editor.
const active = document.activeElement
const isNeutralFocus =
active === null || active === document.body || (rootEl?.contains(active) ?? false)
if (!isNeutralFocus) {
return
}
// Why: pass 'start' (not null) to resolve to a proper TextSelection at
// doc position 1. With null, Tiptap keeps whatever the editor's current
// selection happens to be on mount — for a freshly-created empty doc
// that's an AllSelection, which renders as a visible 0-width highlight
// inside the placeholder instead of a normal blinking caret.
//
// Why: `scrollIntoView: false` prevents Tiptap's focus command from
// scrolling the cursor into view, which would otherwise race with
// useEditorScrollRestore's RAF retry loop and clobber the cached
// scroll position on every tab switch.
nextEditor.commands.focus('start', { scrollIntoView: false })
})
}