Skip to content

code block: copy and language selection#2871

Open
Soxasora wants to merge 7 commits intostackernews:masterfrom
Soxasora:feat/select-code-block-language
Open

code block: copy and language selection#2871
Soxasora wants to merge 7 commits intostackernews:masterfrom
Soxasora:feat/select-code-block-language

Conversation

@Soxasora
Copy link
Member

@Soxasora Soxasora commented Mar 18, 2026

Post #2865

Description

Closes #2857
Adds code actions on code block hover:

  • copy
  • language selector
    • selection disabled in reader mode

Screenshots

languageselector

image image

Additional Context

tbd

Checklist

Are your changes backward compatible? Please answer below:

Yes

On a scale of 1-10 how well and how have you QA'd this change and any features it might affect? Please answer below:

6, in QA

For frontend changes: Tested on mobile, light and dark mode? Please answer below:

Yes, tested on mobile Safari and Chrome, in both light and dark mode.

Did you introduce any new environment variables? If so, call them out explicitly here:
n/a

Did you use AI for this? If so, how much did it assist you?
Porting and refactor of the dom injection logic from the original PR


Note

Medium Risk
Introduces new global mouse event handling and Lexical mutation listeners to show a hover UI over code blocks; risk is mainly UI/interaction regressions and performance in documents with many code blocks.

Overview
Adds a new CodeActionMenuPlugin that appears when hovering code blocks, showing a copy button and a language selector (disabled in read-only/reader mode).

Wires this plugin into both the rich editor (editor.js) and the reader (reader.js), adds new CSS for the hover UI, and factors the dropdown portal helper into a shared MenuAlternateDimension utility used by both the toolbar and the new code actions menu.

Written by Cursor Bugbot for commit a6ef0a1. This will update automatically on new commits. Configure here.

@Soxasora Soxasora changed the title Feat/select code block language code block: copy and language selection Mar 18, 2026
@Soxasora Soxasora added feature new product features that weren't there before editor labels Mar 18, 2026
@Soxasora Soxasora marked this pull request as ready for review March 19, 2026 16:09
/>
)
: <span className={codeStyles.codeActionLanguage}>{getLanguageFriendlyName(lang)}</span>}
<CopyButton icon value={getCodeContent()} />
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy button captures stale code content at render

Low Severity

getCodeContent() is eagerly evaluated at render time and passed as a static value prop to CopyButton. The CodeActionMenuContainer only re-renders when isShown, lang, or position state changes — not when the code block's text content changes. If the user edits the code block content while the action menu is visible (e.g., via keyboard while mouse hovers over the copy button area), clicking copy will capture stale content from the last render rather than current content.

Additional Locations (1)
Fix in Cursor Fix in Web

Copy link
Member Author

@Soxasora Soxasora Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lexical selection is not lost by clicking quote reply and getCodeContent() creates a live read transaction in the editor (the reader in this case):

 const getCodeContent = useCallback(() => {
    const domNode = codeDOMNodeRef.current
    if (!domNode) return ''
    let content = ''
    editor.read(() => {
      const codeNode = getCodeNodeFromDOMNode(domNode)
      if (codeNode) content = codeNode.getTextContent()
    })
    return content
  }, [editor])

right: `${editorRight - right + CODE_PADDING}px`,
top: `${y - editorY}px`
})
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Menu not hidden when code node belongs to different editor

Low Severity

When handleMouseMove finds a codeDOMNode via DOM query but getCodeNodeFromDOMNode returns null (the node isn't in this editor's Lexical tree), the handler falls through without hiding the menu. Additionally, codeDOMNodeRef.current is updated on line 169 before validating the node belongs to this editor. In multi-reader pages (e.g., a feed with multiple posts containing code blocks), hovering from one reader's code block to another's leaves a stale menu visible and points codeDOMNodeRef at the wrong code block, causing getCodeContent() to return an empty string on copy.

Fix in Cursor Fix in Web

@huumn
Copy link
Member

huumn commented Mar 22, 2026

This looks good to me and works well.

My bot found issues, but I don't think they're worth blocking for as they seem to be issues beyond the scope of the PR (generic to linebreaks) or assume many simultaneous code editors:

I found 3 concrete risks against master:

  1. High: multiline selection -> code block conversion can drop content after the first soft line break.
    In lib/lexical/nodes/utils.js, $splitParagraphsByLineBreaks() rebuilds the selection using stale {key, offset, type} values after structurally splitting the paragraph. lib/lexical/commands/formatting/blocks.js then reads selection.getTextContent() from that mutated selection, so converting a multiline paragraph to a code block can silently truncate the selected text.
  2. Medium: the new code-block hover/copy UI is not scoped to a single editor/reader instance.
    In components/editor/plugins/code/actions.js, hover state is driven by a global document mousemove handler and resolves any code.sn-code-block it sees. On pages with multiple readers/editors, moving between different code blocks can leave stale hover UI visible and make copy/language actions resolve against the wrong editor state.
  3. Medium: the new hover UI has a scaling/perf risk on code-heavy pages.
    Each mounted reader/editor with at least one CodeNode registers its own document.mousemove listener in components/editor/plugins/code/actions.js. On pages with many rendered code blocks, this creates avoidable global event work, repeated DOM lookups, and repeated getBoundingClientRect() calls during mouse movement.

lmk

@Soxasora
Copy link
Member Author

I still need to verify the other claims but the 2nd is actually a problem I didn't foresee. If you open the code language selector on codeblock1 but you then move the cursor to codeblock3, selecting will apply the changes to codeblock3 and not codeblock1:

confused.codeblocks.mov

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

setFilter('')
onClose()
}
}, [open, isEditingRef, onClose])
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Language close handler can trigger render loop

High Severity

LanguageSelector effect depends on onClose, but parent passes a new inline onClose each render. When closed, effect calls onClose, which can call handleMouseMove and update position state, causing another render and another new onClose, repeatedly.

Additional Locations (1)
Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

editor feature new product features that weren't there before

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support language selection in code blocks

2 participants