close-bracket-config.ts 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. import { CloseBracketConfig } from '../../extensions/close-brackets'
  2. import {
  3. codePointAt,
  4. codePointSize,
  5. EditorState,
  6. SelectionRange,
  7. Text,
  8. } from '@codemirror/state'
  9. import { completionStatus } from '@codemirror/autocomplete'
  10. export const closeBracketConfig: CloseBracketConfig = {
  11. brackets: ['$', '$$', '[', '{', '('],
  12. buildInsert(
  13. state: EditorState,
  14. range: SelectionRange,
  15. open: string,
  16. close: string
  17. ): string {
  18. switch (open) {
  19. // close for $ or $$
  20. case '$': {
  21. const prev = prevChar(state.doc, range.head)
  22. if (prev === '\\') {
  23. const preprev = prevChar(state.doc, range.head - prev.length)
  24. // add an unprefixed closing dollar to \\$
  25. if (preprev === '\\') {
  26. return open + '$'
  27. }
  28. // don't auto-close \$
  29. return open
  30. }
  31. // avoid creating an odd number of dollar signs
  32. const count = countSurroundingCharacters(state.doc, range.from, open)
  33. if (count % 2 !== 0) {
  34. return open
  35. }
  36. return open + close
  37. }
  38. // close for [ or \[
  39. case '[': {
  40. const prev = prevChar(state.doc, range.head)
  41. if (prev === '\\') {
  42. const preprev = prevChar(state.doc, range.head - prev.length)
  43. // add an unprefixed closing bracket to \\[
  44. if (preprev === '\\') {
  45. return open + ']'
  46. }
  47. return open + '\\' + close
  48. }
  49. return open + close
  50. }
  51. // only close for \(
  52. case '(': {
  53. const prev = prevChar(state.doc, range.head)
  54. if (prev === '\\') {
  55. const preprev = prevChar(state.doc, range.head - prev.length)
  56. // don't auto-close \\(
  57. if (preprev === '\\') {
  58. return open
  59. }
  60. return open + '\\' + close
  61. }
  62. return open
  63. }
  64. // only close for {
  65. case '{': {
  66. const prev = prevChar(state.doc, range.head)
  67. if (prev === '\\') {
  68. const preprev = prevChar(state.doc, range.head - prev.length)
  69. // add an unprefixed closing bracket to \\{
  70. if (preprev === '\\') {
  71. return open + '}'
  72. }
  73. // don't auto-close \{
  74. return open
  75. }
  76. // avoid auto-closing curly brackets when autocomplete is open
  77. if (completionStatus(state)) {
  78. return open
  79. }
  80. return open + close
  81. }
  82. default:
  83. return open + close
  84. }
  85. },
  86. }
  87. function prevChar(doc: Text, pos: number) {
  88. const prev = doc.sliceString(pos - 2, pos)
  89. return codePointSize(codePointAt(prev, 0)) === prev.length
  90. ? prev
  91. : prev.slice(1)
  92. }
  93. function countSurroundingCharacters(doc: Text, pos: number, insert: string) {
  94. let count = 0
  95. // count backwards
  96. let to = pos
  97. do {
  98. const char = doc.sliceString(to - insert.length, to)
  99. if (char !== insert) {
  100. break
  101. }
  102. count++
  103. to--
  104. } while (to > 1)
  105. // count forwards
  106. let from = pos
  107. do {
  108. const char = doc.sliceString(from, from + insert.length)
  109. if (char !== insert) {
  110. break
  111. }
  112. count++
  113. from++
  114. } while (from < doc.length)
  115. return count
  116. }