Merge pull request #32135 from overleaf/mg-comment-empty-select

Allow adding comments on empty selection

GitOrigin-RevId: 14e742bb563fab99624be860691f1a9d2dabc00e
This commit is contained in:
Malik Glossop
2026-03-20 09:07:28 +00:00
committed by Copybot
parent 504005aa74
commit 6901a2a8de
9 changed files with 238 additions and 13 deletions
@@ -0,0 +1,18 @@
import { EditorState } from '@codemirror/state'
/**
* Returns true when the cursor is on an empty line with no active selection.
*
* Used to disable the "Add comment" action — there is nothing to anchor a
* comment to on a blank line.
*/
export function isCursorOnEmptyLine(state: EditorState): boolean {
const { main } = state.selection
if (!main.empty) {
return false
}
const line = state.doc.lineAt(main.head)
return line.text.trim().length === 0
}
@@ -0,0 +1,68 @@
import { EditorState } from '@codemirror/state'
import { isCursorOnEmptyLine } from './is-cursor-on-empty-line'
/**
* Returns the current highlighted range, or the nearest non-whitespace token
* to the cursor when there is no active selection.
*
* "Token" here means any contiguous run of non-whitespace characters (`/\S+/`),
* not necessarily an alphanumeric word. When two tokens are equidistant the
* earlier one (closer to document start) wins.
*
* Only searches the current line.
*
* Only called on user-initiated actions (e.g. "Add comment"), not on every render.
* Returns null when the cursor is on an empty/whitespace-only line.
*/
export function selectHighlightedOrNearestToken(
state: EditorState
): { from: number; to: number } | null {
if (isCursorOnEmptyLine(state)) {
return null
}
const { main } = state.selection
if (!main.empty) {
return { from: main.from, to: main.to }
}
const pos = main.head
const line = state.doc.lineAt(pos)
let bestFrom = 0
let bestTo = 0
let bestDist = Infinity
for (const match of line.text.matchAll(/\S+/g)) {
const from = line.from + match.index!
const to = from + match[0].length
let dist = 0
if (pos < from) {
dist = from - pos
} else if (pos > to) {
dist = pos - to
}
if (dist === 0) {
return { from, to }
}
if (dist < bestDist) {
bestFrom = from
bestTo = to
bestDist = dist
}
// Past the cursor — no closer match possible on this line
if (from > pos) {
break
}
}
if (bestDist < Infinity) {
return { from: bestFrom, to: bestTo }
}
return null
}