mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 17:19:37 +02:00
Reference picker
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
import { keymap } from '@codemirror/view'
|
||||
import { Prec, Extension } from '@codemirror/state'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import { startCompletion } from '@codemirror/autocomplete'
|
||||
import { getBibkeyArgumentNode } from '@/features/source-editor/utils/tree-operations/ancestors'
|
||||
|
||||
/**
|
||||
* Parse cite-argument text into non-separator tokens with their
|
||||
* start/end positions relative to the beginning of the text.
|
||||
* Separators are commas and whitespace (space, tab, newline).
|
||||
*/
|
||||
function parseTokens(
|
||||
text: string
|
||||
): { value: string; start: number; end: number }[] {
|
||||
const tokens: { value: string; start: number; end: number }[] = []
|
||||
const re = /[^\s,]+/g
|
||||
let m
|
||||
while ((m = re.exec(text)) !== null) {
|
||||
tokens.push({ value: m[0], start: m.index, end: m.index + m[0].length })
|
||||
}
|
||||
return tokens
|
||||
}
|
||||
|
||||
function openPickerIfInCite(view: EditorView): boolean {
|
||||
try {
|
||||
const mainSel = view.state.selection.main
|
||||
const pos = mainSel.head
|
||||
const node = getBibkeyArgumentNode(view.state, pos)
|
||||
if (node) {
|
||||
// Verify the node has proper matching braces — the tree parser
|
||||
// may produce incorrect boundaries during error recovery (e.g.
|
||||
// unclosed \cite{ or nested braces like {\bf ...}).
|
||||
const openChar = view.state.doc.sliceString(node.from, node.from + 1)
|
||||
const closeChar = view.state.doc.sliceString(node.to - 1, node.to)
|
||||
if (openChar !== '{' || closeChar !== '}') {
|
||||
return startCompletion(view)
|
||||
}
|
||||
|
||||
const braceFrom = node.from + 1
|
||||
const braceTo = node.to - 1
|
||||
|
||||
// Cursor must be inside the braces (between braceFrom and braceTo inclusive)
|
||||
if (pos < braceFrom || pos > braceTo) {
|
||||
return startCompletion(view)
|
||||
}
|
||||
|
||||
// Clamp selection to brace boundaries
|
||||
const selFrom = Math.max(braceFrom, mainSel.from)
|
||||
const selTo = Math.min(braceTo, mainSel.to)
|
||||
const hasSelection = selFrom < selTo
|
||||
|
||||
if (hasSelection) {
|
||||
const selectedText = view.state.doc.sliceString(selFrom, selTo)
|
||||
|
||||
// Don't open modal if the selection contains unpaired braces
|
||||
const opens = (selectedText.match(/{/g) || []).length
|
||||
const closes = (selectedText.match(/}/g) || []).length
|
||||
if (opens !== closes) {
|
||||
return startCompletion(view)
|
||||
}
|
||||
|
||||
// Parse the full cite content into tokens for partial-key expansion
|
||||
const fullContent = view.state.doc.sliceString(braceFrom, braceTo)
|
||||
const tokens = parseTokens(fullContent)
|
||||
|
||||
// Relative selection offsets within the cite content
|
||||
const relSelFrom = selFrom - braceFrom
|
||||
const relSelTo = selTo - braceFrom
|
||||
|
||||
// Find tokens overlapping the selection and expand boundaries
|
||||
let expandedFrom = relSelFrom
|
||||
let expandedTo = relSelTo
|
||||
const selectedTokenValues: string[] = []
|
||||
|
||||
for (const token of tokens) {
|
||||
if (token.end > relSelFrom && token.start < relSelTo) {
|
||||
expandedFrom = Math.min(expandedFrom, token.start)
|
||||
expandedTo = Math.max(expandedTo, token.end)
|
||||
selectedTokenValues.push(token.value)
|
||||
}
|
||||
}
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('reference:openPicker', {
|
||||
detail: {
|
||||
braceFrom,
|
||||
braceTo,
|
||||
insertFrom: braceFrom + expandedFrom,
|
||||
insertTo: braceFrom + expandedTo,
|
||||
selectedTokens: selectedTokenValues,
|
||||
},
|
||||
})
|
||||
)
|
||||
} else {
|
||||
// No selection — insert at cursor position
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('reference:openPicker', {
|
||||
detail: {
|
||||
braceFrom,
|
||||
braceTo,
|
||||
insertFrom: pos,
|
||||
insertTo: pos,
|
||||
selectedTokens: [],
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
} catch {
|
||||
// fall through to default completion
|
||||
}
|
||||
return startCompletion(view)
|
||||
}
|
||||
|
||||
export const extension = (): Extension =>
|
||||
Prec.highest(
|
||||
keymap.of([
|
||||
{ key: 'Ctrl-Space', run: openPickerIfInCite },
|
||||
{ key: 'Alt-Space', run: startCompletion },
|
||||
])
|
||||
)
|
||||
Reference in New Issue
Block a user