|
@@ -105,11 +105,39 @@ function findPattern(
|
|
|
prevSession: PatternSession | null,
|
|
prevSession: PatternSession | null,
|
|
|
): PatternSession | null {
|
|
): PatternSession | null {
|
|
|
const { $head } = state.selection
|
|
const { $head } = state.selection
|
|
|
- if (!$head.parent.isTextblock) return null
|
|
|
|
|
|
|
+ const parentNode = $head.parent
|
|
|
|
|
+ if (!parentNode || !parentNode.isTextblock) return null
|
|
|
|
|
+
|
|
|
|
|
+ // Build layout-aware textBefore by iterating nodes
|
|
|
|
|
+ // This respects hard_break boundaries instead of flattening via textContent
|
|
|
|
|
+ let textBefore = ""
|
|
|
|
|
+ let currentOffset = 0
|
|
|
|
|
+ const targetOffset = $head.parentOffset
|
|
|
|
|
+
|
|
|
|
|
+ for (let i = 0; i < parentNode.childCount; i++) {
|
|
|
|
|
+ const child = parentNode.child(i)
|
|
|
|
|
+
|
|
|
|
|
+ if (currentOffset >= targetOffset) break
|
|
|
|
|
+
|
|
|
|
|
+ if (child.type.name === "hard_break") {
|
|
|
|
|
+ // Hard break resets the current line
|
|
|
|
|
+ textBefore = ""
|
|
|
|
|
+ currentOffset += child.nodeSize
|
|
|
|
|
+ } else if (child.isText && child.text) {
|
|
|
|
|
+ const remainingSpace = targetOffset - currentOffset
|
|
|
|
|
+ if (remainingSpace <= child.text.length) {
|
|
|
|
|
+ textBefore += child.text.slice(0, remainingSpace)
|
|
|
|
|
+ break
|
|
|
|
|
+ } else {
|
|
|
|
|
+ textBefore += child.text
|
|
|
|
|
+ currentOffset += child.text.length
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ currentOffset += child.nodeSize
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- const text = $head.parent.textContent
|
|
|
|
|
- const offset = $head.parentOffset
|
|
|
|
|
- const textBefore = text.slice(0, offset)
|
|
|
|
|
|
|
+ const offset = textBefore.length
|
|
|
|
|
|
|
|
for (const spec of PATTERN_SPECS) {
|
|
for (const spec of PATTERN_SPECS) {
|
|
|
const match = matchPattern(textBefore, spec)
|
|
const match = matchPattern(textBefore, spec)
|
|
@@ -117,15 +145,30 @@ function findPattern(
|
|
|
|
|
|
|
|
const { startIdx, trigger, query } = match
|
|
const { startIdx, trigger, query } = match
|
|
|
const startPos = $head.start() + startIdx
|
|
const startPos = $head.start() + startIdx
|
|
|
-
|
|
|
|
|
if (isPatternCompleted(spec, query)) return null
|
|
if (isPatternCompleted(spec, query)) return null
|
|
|
|
|
|
|
|
// For delimiter-terminated patterns, check if the closing delimiter
|
|
// For delimiter-terminated patterns, check if the closing delimiter
|
|
|
- // exists anywhere in the full textblock after the trigger start.
|
|
|
|
|
|
|
+ // exists anywhere in the textblock after the trigger start.
|
|
|
// This prevents an active session when the pattern is already complete
|
|
// This prevents an active session when the pattern is already complete
|
|
|
// but the cursor is inside it (e.g. navigating into [[Page Ref]]).
|
|
// but the cursor is inside it (e.g. navigating into [[Page Ref]]).
|
|
|
if (spec.termination === "delimiter" && spec.end) {
|
|
if (spec.termination === "delimiter" && spec.end) {
|
|
|
- const textFromTrigger = text.slice(startIdx)
|
|
|
|
|
|
|
+ // Build text from trigger position to end of textblock, respecting hard_breaks
|
|
|
|
|
+ let textFromTrigger = ""
|
|
|
|
|
+ let triggerFound = false
|
|
|
|
|
+ for (let i = 0; i < parentNode.childCount; i++) {
|
|
|
|
|
+ const child = parentNode.child(i)
|
|
|
|
|
+ if (child.type.name === "hard_break") {
|
|
|
|
|
+ textFromTrigger += "\n"
|
|
|
|
|
+ } else if (child.isText && child.text) {
|
|
|
|
|
+ if (triggerFound) {
|
|
|
|
|
+ textFromTrigger += child.text
|
|
|
|
|
+ } else if (child.text.includes(trigger)) {
|
|
|
|
|
+ triggerFound = true
|
|
|
|
|
+ const triggerIdx = child.text.indexOf(trigger)
|
|
|
|
|
+ textFromTrigger = child.text.slice(triggerIdx + trigger.length)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
if (textFromTrigger.includes(spec.end)) return null
|
|
if (textFromTrigger.includes(spec.end)) return null
|
|
|
}
|
|
}
|
|
|
|
|
|