ranges.ts 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760
  1. import { EditorView } from '@codemirror/view'
  2. import { EditorSelection, EditorState, SelectionRange } from '@codemirror/state'
  3. import {
  4. ensureSyntaxTree,
  5. foldedRanges,
  6. foldEffect,
  7. syntaxTree,
  8. } from '@codemirror/language'
  9. import { SyntaxNode } from '@lezer/common'
  10. import {
  11. ancestorOfNodeWithType,
  12. isUnknownCommandWithName,
  13. } from '../utils/tree-query'
  14. import { lastAncestorAtEndPosition } from '../utils/tree-operations/ancestors'
  15. export const wrapRanges =
  16. (
  17. prefix: string,
  18. suffix: string,
  19. wrapWholeLine = false,
  20. selection?: (range: SelectionRange, view: EditorView) => SelectionRange
  21. ) =>
  22. (view: EditorView): boolean => {
  23. if (view.state.readOnly) {
  24. return false
  25. }
  26. view.dispatch(
  27. view.state.changeByRange(range => {
  28. const insert = { prefix, suffix }
  29. if (wrapWholeLine) {
  30. const line = view.state.doc.lineAt(range.anchor)
  31. if (range.empty) {
  32. // expand range to cover the whole line
  33. range = EditorSelection.range(line.from, line.to)
  34. }
  35. // add a newline at the start if needed
  36. if (range.from !== line.from) {
  37. insert.prefix = `\n${prefix}`
  38. }
  39. // add a newline at the end if needed
  40. if (range.to !== line.to) {
  41. insert.suffix = `${suffix}\n`
  42. }
  43. }
  44. const content = view.state.sliceDoc(range.from, range.to)
  45. // map through the prefix only
  46. const changedRange = range.map(
  47. view.state.changes([
  48. { from: range.from, insert: `${insert.prefix}` },
  49. ]),
  50. 1
  51. )
  52. return {
  53. range: selection ? selection(changedRange, view) : changedRange,
  54. // create a single change, including the content
  55. changes: [
  56. {
  57. from: range.from,
  58. to: range.to,
  59. insert: `${insert.prefix}${content}${insert.suffix}`,
  60. },
  61. ],
  62. }
  63. }),
  64. { scrollIntoView: true }
  65. )
  66. return true
  67. }
  68. export const changeCase =
  69. (upper = true) =>
  70. (view: EditorView) => {
  71. if (view.state.readOnly) {
  72. return false
  73. }
  74. view.dispatch(
  75. view.state.changeByRange(range => {
  76. // ignore empty ranges
  77. if (range.empty) {
  78. return { range }
  79. }
  80. const text = view.state.doc.sliceString(range.from, range.to)
  81. return {
  82. range,
  83. changes: [
  84. {
  85. from: range.from,
  86. to: range.to,
  87. insert: upper ? text.toUpperCase() : text.toLowerCase(),
  88. },
  89. ],
  90. }
  91. })
  92. )
  93. return true
  94. }
  95. export const duplicateSelection = (view: EditorView) => {
  96. if (view.state.readOnly) {
  97. return false
  98. }
  99. const foldedRangesInDocument = foldedRanges(view.state)
  100. view.dispatch(
  101. view.state.changeByRange(range => {
  102. const folds: { offset: number; base: number; length: number }[] = []
  103. if (range.empty) {
  104. const line = view.state.doc.lineAt(range.from)
  105. let lineStart = line.from
  106. let lineEnd = line.to
  107. // Calculate line start/end including folded ranges
  108. //
  109. // Note that at each iteration of the while loop, new folded ranges
  110. // can be included. This happens when there are multiple folded ranges
  111. // on a single editor line (but spanning multiple actual lines)
  112. //
  113. // For example, the following document:
  114. // 1: \begin{document}
  115. // 2: test
  116. // 3: \end{document}\begin{document}
  117. // 4: test
  118. // 5: \end{document}
  119. //
  120. // Can be folded to:
  121. // \begin{document}<...>\end{document}\begin{document}<...>\end{document}
  122. //
  123. // In this case, the first iterations of the while loop below will only
  124. // include lines 3-5, since the overlapping folded range for line 5
  125. // is only the fold on lines 3-5. Hence in the while loop, we expand the
  126. // selection until we include all the ranges.
  127. let changed
  128. do {
  129. changed = false
  130. foldedRangesInDocument.between(lineStart, lineEnd, (from, to) => {
  131. const newLineStart = Math.min(
  132. view.state.doc.lineAt(from).from,
  133. lineStart
  134. )
  135. const newLineEnd = Math.max(view.state.doc.lineAt(to).to, lineEnd)
  136. if (newLineStart !== lineStart || newLineEnd !== lineEnd) {
  137. changed = true
  138. lineStart = newLineStart
  139. lineEnd = newLineEnd
  140. }
  141. })
  142. } while (changed)
  143. // Collect information needed to fold duplicated lines
  144. foldedRangesInDocument.between(lineStart, lineEnd, (from, to) => {
  145. folds.push({
  146. offset: from - lineStart,
  147. base: lineEnd + view.state.lineBreak.length,
  148. length: to - from,
  149. })
  150. })
  151. // Duplicate the selected lines downwards
  152. return {
  153. range,
  154. changes: [
  155. {
  156. from: lineEnd,
  157. insert:
  158. view.state.lineBreak + view.state.doc.slice(lineStart, lineEnd),
  159. },
  160. ],
  161. // Add a fold effect for each fold in the original line
  162. effects: folds.map(fold =>
  163. foldEffect.of({
  164. from: fold.base + fold.offset,
  165. to: fold.base + fold.offset + fold.length,
  166. })
  167. ),
  168. }
  169. } else {
  170. // Duplicate selected text at head of selection
  171. let newSelectionRange = range
  172. if (range.head > range.anchor) {
  173. // Duplicating to the right, so we need to update the selected range
  174. newSelectionRange = EditorSelection.range(
  175. range.head,
  176. range.head + (range.to - range.from)
  177. )
  178. }
  179. return {
  180. // The new range is the duplicated section, placed at the head of the
  181. // original selection
  182. range: newSelectionRange,
  183. changes: [
  184. {
  185. from: range.head,
  186. insert: view.state.doc.slice(range.from, range.to),
  187. },
  188. ],
  189. }
  190. }
  191. })
  192. )
  193. return true
  194. }
  195. function getParentNode(
  196. position: number | SyntaxNode,
  197. state: EditorState,
  198. assoc: 0 | 1 | -1 = 1
  199. ): SyntaxNode | undefined {
  200. const tree = ensureSyntaxTree(state, 1000)
  201. let node: SyntaxNode | undefined | null = null
  202. if (typeof position === 'number') {
  203. node = tree?.resolveInner(position, assoc)?.parent
  204. const ancestorAtEndPos = lastAncestorAtEndPosition(node, position)
  205. if (ancestorAtEndPos?.parent) {
  206. node = ancestorAtEndPos.parent
  207. }
  208. } else {
  209. node = position?.parent
  210. }
  211. while (
  212. ['LongArg', 'TextArgument', 'OpenBrace', 'CloseBrace'].includes(
  213. node?.type.name || ''
  214. )
  215. ) {
  216. node = node!.parent
  217. }
  218. return node || undefined
  219. }
  220. function wrapRangeInCommand(
  221. state: EditorState,
  222. range: SelectionRange,
  223. command: string
  224. ) {
  225. const content = state.sliceDoc(range.from, range.to)
  226. const changes = state.changes([
  227. {
  228. from: range.from,
  229. to: range.to,
  230. insert: `${command}{${content}}`,
  231. },
  232. ])
  233. return {
  234. changes,
  235. range: moveRange(
  236. range,
  237. range.from + command.length + 1,
  238. range.from + command.length + content.length + 1
  239. ),
  240. }
  241. }
  242. function moveRange(range: SelectionRange, newFrom: number, newTo: number) {
  243. const forwards = range.from === range.anchor
  244. return forwards
  245. ? EditorSelection.range(newFrom, newTo)
  246. : EditorSelection.range(newTo, newFrom)
  247. }
  248. function validateReplacement(expected: string, actual: string) {
  249. if (expected !== actual) {
  250. throw new Error('Replacement in toggleRange failed validation.')
  251. }
  252. }
  253. function getWrappingAncestor(
  254. node: SyntaxNode,
  255. command: string,
  256. state: EditorState
  257. ): SyntaxNode | null {
  258. for (
  259. let ancestor: SyntaxNode | null = node;
  260. ancestor;
  261. ancestor = ancestor.parent
  262. ) {
  263. if (isUnknownCommandWithName(ancestor, command, state)) {
  264. return ancestor
  265. }
  266. if (ancestor.type.is('UnknownCommand')) {
  267. // We could multiple levels deep in bold/non-bold. So bail out in this case
  268. return null
  269. }
  270. }
  271. return null
  272. }
  273. function adjustRangeIfNeeded(
  274. command: string,
  275. range: SelectionRange,
  276. state: EditorState
  277. ) {
  278. // Try to adjust the selection, if it is either
  279. // 1. \textbf<{test>}
  280. // 2. \textbf{<test}>
  281. // 3. \textbf<{test}>
  282. // 4. <\textbf{test}>
  283. // 4. \textbf<>{test}
  284. const tree = syntaxTree(state)
  285. if (tree.length < range.to) {
  286. return range
  287. }
  288. const nodeLeft = tree.resolveInner(range.from, 1)
  289. const nodeRight = tree.resolveInner(range.to, -1)
  290. const parentLeft = getWrappingAncestor(nodeLeft, command, state)
  291. const parentRight = getWrappingAncestor(nodeRight, command, state)
  292. const parent = getParentNode(nodeLeft, state)
  293. if (parent?.type.is('UnknownCommand') && spansWholeArgument(parent, range)) {
  294. return bubbleUpRange(
  295. command,
  296. ancestorOfNodeWithType(nodeLeft, 'UnknownCommand'),
  297. range,
  298. state
  299. )
  300. }
  301. if (!parentLeft) {
  302. // We're not trying to unbold, so don't bother adjusting range
  303. return bubbleUpRange(
  304. command,
  305. ancestorOfNodeWithType(nodeLeft, 'UnknownCommand'),
  306. range,
  307. state
  308. )
  309. }
  310. if (nodeLeft.type.is('CtrlSeq') && range.from === range.to) {
  311. const command = nodeLeft.parent?.parent
  312. if (!command) {
  313. return range
  314. }
  315. return EditorSelection.cursor(command.from)
  316. }
  317. let { from, to } = range
  318. if (nodeLeft.type.is('CtrlSeq')) {
  319. from = nodeLeft.to + 1
  320. }
  321. if (nodeLeft.type.is('OpenBrace')) {
  322. from = nodeLeft.to
  323. }
  324. // We know that parentLeft is the UnknownCommand, so now we check if we're
  325. // to the right of the closing brace. (parent is TextArgument, grandparent is
  326. // UnknownCommand)
  327. if (parentLeft === parentRight && nodeRight.type.is('CloseBrace')) {
  328. to = nodeRight.from
  329. }
  330. return bubbleUpRange(command, parentLeft, moveRange(range, from, to), state)
  331. }
  332. function spansWholeArgument(
  333. commandNode: SyntaxNode | null,
  334. range: SelectionRange
  335. ): boolean {
  336. const argument = commandNode?.getChild('TextArgument')?.getChild('LongArg')
  337. const res = Boolean(
  338. argument && argument.from === range.from && argument.to === range.to
  339. )
  340. return res
  341. }
  342. function bubbleUpRange(
  343. command: string,
  344. node: SyntaxNode | null,
  345. range: SelectionRange,
  346. state: EditorState
  347. ) {
  348. let currentRange = range
  349. for (
  350. let ancestorCommand: SyntaxNode | null = ancestorOfNodeWithType(
  351. node,
  352. 'UnknownCommand'
  353. );
  354. spansWholeArgument(ancestorCommand, currentRange);
  355. ancestorCommand = ancestorOfNodeWithType(
  356. ancestorCommand.parent,
  357. 'UnknownCommand'
  358. )
  359. ) {
  360. if (!ancestorCommand) {
  361. break
  362. }
  363. currentRange = moveRange(
  364. currentRange,
  365. ancestorCommand.from,
  366. ancestorCommand.to
  367. )
  368. if (isUnknownCommandWithName(ancestorCommand, command, state)) {
  369. const argumentNode = ancestorCommand
  370. .getChild('TextArgument')
  371. ?.getChild('LongArg')
  372. if (!argumentNode) {
  373. return range
  374. }
  375. return moveRange(range, argumentNode.from, argumentNode.to)
  376. }
  377. }
  378. return range
  379. }
  380. export function toggleRanges(command: string) {
  381. /* There are a number of situations we need to handle in this function.
  382. * In the following examples, the selection range is marked within <>
  383. * 1. If the parent node at start and end of selection is different -> do
  384. * nothing & show error. Case 8 is an exception to this.
  385. * -> For good and bad, this disallows \textbf{this <is} weird
  386. * \textit{to> do}
  387. * 2. If selection contains a BlankLine (i.e. two newlines in a row) -> do
  388. * nothing & show error
  389. * -> \textbf doesn't allow paragraph breaks).
  390. * 3. If the selection is not within a \textbf -> wrap it in a \textbf
  391. * 4. If selection is at the beginning of a \textbf -> shrink the \textbf
  392. * command
  393. * -> e.g. \textbf{<this is> a test} → <this is>\textbf{ a test}
  394. * 5. Similarly for selection at end of \textbf command
  395. * 6. If selection is in the middle of a \textbf command -> split the command
  396. * into two
  397. * -> e.g. \textbf{this <is a> test} → \textbf{this }<is a>\textbf{ test}
  398. * 7. If the selection is a whole \textbf command → remove the wrapping
  399. * command.
  400. * 8. If the selection spans two \textbf's with the same parent then join the
  401. * two
  402. * -> e.g. \textbf{this <is} a \textbf{te>st} → \textbf{this <is a te>st}
  403. * 9. If the selection spans the end of a \textbf, into the parent of the
  404. * command, then extend the \textbf.
  405. * -> e.g. \textbf{this <is} a test> → \textbf{this <is a test>}
  406. * 10. Similarly for selections spanning the beginning of the selection
  407. */
  408. return (view: EditorView): boolean => {
  409. if (view.state.readOnly) {
  410. return false
  411. }
  412. view.dispatch(
  413. view.state.changeByRange(initialRange => {
  414. const range = adjustRangeIfNeeded(command, initialRange, view.state)
  415. const content = view.state.sliceDoc(range.from, range.to)
  416. const ancestorAtStartOfRange = getParentNode(
  417. range.from,
  418. view.state,
  419. range.from === 0 ? 1 : -1
  420. )
  421. const ancestorAtEndOfRange = range.empty
  422. ? ancestorAtStartOfRange
  423. : getParentNode(
  424. range.to,
  425. view.state,
  426. range.to === view.state.doc.length ? -1 : 1
  427. )
  428. const tree = ensureSyntaxTree(view.state, 1000)
  429. const nodeAtFrom = tree?.resolveInner(range.from, 1)
  430. const nodeAtTo = tree?.resolveInner(range.to, -1)
  431. const isSingleNodeSelected = nodeAtFrom === nodeAtTo
  432. if (
  433. !isSingleNodeSelected &&
  434. ancestorAtStartOfRange !== ancestorAtEndOfRange
  435. ) {
  436. // But handle the exception of case 8
  437. const ancestorAtStartIsWrappingCommand =
  438. ancestorAtStartOfRange &&
  439. isUnknownCommandWithName(
  440. ancestorAtStartOfRange,
  441. command,
  442. view.state
  443. )
  444. const ancestorAtEndIsWrappingCommand =
  445. ancestorAtEndOfRange &&
  446. isUnknownCommandWithName(ancestorAtEndOfRange, command, view.state)
  447. if (
  448. ancestorAtStartIsWrappingCommand &&
  449. ancestorAtEndIsWrappingCommand &&
  450. ancestorAtStartOfRange?.parent?.parent &&
  451. ancestorAtEndOfRange?.parent?.parent
  452. ) {
  453. // Test for case 8
  454. const nextAncestorAtStartOfRange =
  455. ancestorAtStartOfRange.parent.parent
  456. const nextAncestorAtEndOfRange = ancestorAtEndOfRange.parent.parent
  457. if (nextAncestorAtStartOfRange === nextAncestorAtEndOfRange) {
  458. // Join the two ranges
  459. const textBetweenRanges = view.state.sliceDoc(
  460. ancestorAtStartOfRange.to,
  461. ancestorAtEndOfRange.from
  462. )
  463. const ancestorStartArgumentNode =
  464. ancestorAtStartOfRange.lastChild?.getChild('LongArg')
  465. const ancestorEndArgumentNode =
  466. ancestorAtEndOfRange.lastChild?.getChild('LongArg')
  467. if (!ancestorStartArgumentNode || !ancestorEndArgumentNode) {
  468. throw new Error("Can't find argument node")
  469. }
  470. const actualContent = view.state.sliceDoc(
  471. ancestorAtStartOfRange.from,
  472. ancestorAtEndOfRange.to
  473. )
  474. const firstCommandArgument = view.state.sliceDoc(
  475. ancestorStartArgumentNode.from,
  476. ancestorStartArgumentNode.to
  477. )
  478. const secondCommandArgument = view.state.sliceDoc(
  479. ancestorEndArgumentNode.from,
  480. ancestorEndArgumentNode.to
  481. )
  482. validateReplacement(
  483. `${command}{${firstCommandArgument}}${textBetweenRanges}${command}{${secondCommandArgument}}`,
  484. actualContent
  485. )
  486. const changes = view.state.changes([
  487. {
  488. from: ancestorAtStartOfRange.from,
  489. to: ancestorAtEndOfRange.to,
  490. insert: `${command}{${firstCommandArgument}${textBetweenRanges}${secondCommandArgument}}`,
  491. },
  492. ])
  493. return {
  494. changes,
  495. range: moveRange(
  496. range,
  497. range.from,
  498. range.to - command.length - 1 - 1
  499. ),
  500. }
  501. }
  502. }
  503. if (
  504. ancestorAtEndIsWrappingCommand &&
  505. ancestorAtEndOfRange.parent?.parent === ancestorAtStartOfRange
  506. ) {
  507. // Extend to the left. Case 10
  508. const contentUpToCommand = view.state.sliceDoc(
  509. range.from,
  510. ancestorAtEndOfRange.from
  511. )
  512. const ancestorEndArgumentNode =
  513. ancestorAtEndOfRange.lastChild?.getChild('LongArg')
  514. if (!ancestorEndArgumentNode) {
  515. throw new Error("Can't find argument node")
  516. }
  517. const commandContent = view.state.sliceDoc(
  518. ancestorEndArgumentNode.from,
  519. ancestorEndArgumentNode.to
  520. )
  521. const actualContent = view.state.sliceDoc(
  522. range.from,
  523. ancestorAtEndOfRange.to
  524. )
  525. validateReplacement(
  526. `${contentUpToCommand}${command}{${commandContent}}`,
  527. actualContent
  528. )
  529. const changes = view.state.changes([
  530. {
  531. from: range.from,
  532. to: ancestorAtEndOfRange.to,
  533. insert: `${command}{${contentUpToCommand}${commandContent}}`,
  534. },
  535. ])
  536. return {
  537. changes,
  538. range: moveRange(
  539. range,
  540. range.from + command.length + 1,
  541. range.to
  542. ),
  543. }
  544. }
  545. if (
  546. ancestorAtStartIsWrappingCommand &&
  547. ancestorAtStartOfRange.parent?.parent === ancestorAtEndOfRange
  548. ) {
  549. // Extend to the right. Case 9
  550. const contentAfterCommand = view.state.sliceDoc(
  551. ancestorAtStartOfRange.to,
  552. range.to
  553. )
  554. const ancestorStartArgumentNode =
  555. ancestorAtStartOfRange.lastChild?.getChild('LongArg')
  556. if (!ancestorStartArgumentNode) {
  557. throw new Error("Can't find argument node")
  558. }
  559. const commandContent = view.state.sliceDoc(
  560. ancestorStartArgumentNode.from,
  561. ancestorStartArgumentNode.to
  562. )
  563. const actualContent = view.state.sliceDoc(
  564. ancestorAtStartOfRange.from,
  565. range.to
  566. )
  567. validateReplacement(
  568. `${command}{${commandContent}}${contentAfterCommand}`,
  569. actualContent
  570. )
  571. const changes = view.state.changes([
  572. {
  573. from: ancestorAtStartOfRange.from,
  574. to: range.to,
  575. insert: `${command}{${commandContent}${contentAfterCommand}}`,
  576. },
  577. ])
  578. return {
  579. changes,
  580. range: moveRange(range, range.from, range.to - 1),
  581. }
  582. }
  583. // Bail out in case 1
  584. // TODO: signal error to the user
  585. return { range }
  586. }
  587. const ancestor = ancestorAtStartOfRange
  588. // Bail out in case 2
  589. if (content.includes('\n\n')) {
  590. // TODO: signal error to the user
  591. return { range }
  592. }
  593. const isCursorBeforeAncestor =
  594. range.empty &&
  595. ancestor &&
  596. range.from === ancestor.from &&
  597. isUnknownCommandWithName(ancestor, command, view.state)
  598. // If we can't find an ancestor node, or if the parent is not an exsting
  599. // \textbf, then we just wrap it in a range. Case 3.
  600. if (
  601. isCursorBeforeAncestor ||
  602. !ancestor ||
  603. !isUnknownCommandWithName(ancestor, command, view.state)
  604. ) {
  605. return wrapRangeInCommand(view.state, range, command)
  606. }
  607. const argumentNode = ancestor.lastChild?.getChild('LongArg')
  608. if (!argumentNode) {
  609. throw new Error("Can't find argument node")
  610. }
  611. // We should trim from the beginning. Case 4
  612. if (range.from === argumentNode.from && range.to !== argumentNode.to) {
  613. const textAfterSelection = view.state.sliceDoc(
  614. range.to,
  615. argumentNode.to
  616. )
  617. const wholeCommand = view.state.sliceDoc(ancestor.from, ancestor.to)
  618. validateReplacement(
  619. `${command}{${content}${textAfterSelection}}`,
  620. wholeCommand
  621. )
  622. const changes = view.state.changes([
  623. {
  624. from: ancestor.from,
  625. to: ancestor.to,
  626. insert: `${content}${command}{${textAfterSelection}}`,
  627. },
  628. ])
  629. return {
  630. range: moveRange(
  631. range,
  632. range.from - command.length - 1,
  633. range.to - command.length - 1
  634. ),
  635. changes,
  636. }
  637. }
  638. // We should trim from the end. Case 5
  639. if (range.to === argumentNode.to && range.from !== argumentNode.from) {
  640. const textBeforeSelection = view.state.sliceDoc(
  641. ancestor.from,
  642. range.from
  643. )
  644. const wholeCommand = view.state.sliceDoc(ancestor.from, ancestor.to)
  645. validateReplacement(`${textBeforeSelection}${content}}`, wholeCommand)
  646. const changes = view.state.changes([
  647. {
  648. from: ancestor.from,
  649. to: ancestor.to,
  650. insert: `${textBeforeSelection}}${content}`,
  651. },
  652. ])
  653. // We should shift selection forward by the } we insert
  654. return {
  655. range: moveRange(range, range.from + 1, range.to + 1),
  656. changes,
  657. }
  658. }
  659. // We should split the command in two. Case 6
  660. if (range.from !== argumentNode.from && range.to !== argumentNode.to) {
  661. const textBeforeSelection = view.state.sliceDoc(
  662. ancestor.from,
  663. range.from
  664. )
  665. const textAfterSelection = view.state.sliceDoc(range.to, ancestor.to)
  666. const wholeCommand = view.state.sliceDoc(ancestor.from, ancestor.to)
  667. validateReplacement(
  668. `${textBeforeSelection}${content}${textAfterSelection}`,
  669. wholeCommand
  670. )
  671. const changes = view.state.changes([
  672. {
  673. from: ancestor.from,
  674. to: ancestor.to,
  675. insert: `${textBeforeSelection}}${content}${command}{${textAfterSelection}`,
  676. },
  677. ])
  678. return {
  679. range: moveRange(range, range.from + 1, range.to + 1),
  680. changes,
  681. }
  682. }
  683. // Remove the wrapping command. Case 7
  684. if (spansWholeArgument(ancestor, range)) {
  685. const argumentContent = view.state.sliceDoc(
  686. argumentNode.from,
  687. argumentNode.to
  688. )
  689. const wholeCommand = view.state.sliceDoc(ancestor.from, ancestor.to)
  690. validateReplacement(`${command}{${content}}`, wholeCommand)
  691. const changes = view.state.changes([
  692. { from: ancestor.from, to: ancestor.to, insert: argumentContent },
  693. ])
  694. return {
  695. range: moveRange(
  696. range,
  697. range.from - command.length - 1,
  698. range.to - command.length - 1
  699. ),
  700. changes,
  701. }
  702. }
  703. // Shouldn't happen, but default to just wrapping the content
  704. return wrapRangeInCommand(view.state, range, command)
  705. }),
  706. { scrollIntoView: true }
  707. )
  708. return true
  709. }
  710. }