ranges.ts 24 KB

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