environments.ts 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. import { ensureSyntaxTree } from '@codemirror/language'
  2. import { EditorState } from '@codemirror/state'
  3. import { SyntaxNode, SyntaxNodeRef, Tree } from '@lezer/common'
  4. import { previousSiblingIs } from './common'
  5. import { NodeIntersectsChangeFn, ProjectionItem } from './projection'
  6. const HUNDRED_MS = 100
  7. export class EnvironmentName extends ProjectionItem {
  8. title = ''
  9. }
  10. export const enterNode = (
  11. state: EditorState,
  12. node: SyntaxNodeRef,
  13. items: EnvironmentName[],
  14. nodeIntersectsChange: NodeIntersectsChangeFn
  15. ): any => {
  16. if (node.type.is('EnvNameGroup')) {
  17. if (!nodeIntersectsChange(node.node)) {
  18. return false
  19. }
  20. if (!node.node.prevSibling?.type.is('Begin')) {
  21. return false
  22. }
  23. const openBraceNode = node.node.getChild('OpenBrace')
  24. if (!openBraceNode) {
  25. return false
  26. }
  27. const envNameNode = openBraceNode.node.nextSibling
  28. if (!envNameNode) {
  29. return false
  30. }
  31. const envNameText = state.doc.sliceString(envNameNode.from, envNameNode.to)
  32. if (envNameText.length < 1) {
  33. return false
  34. }
  35. const thisEnvironmentName = {
  36. title: envNameText,
  37. from: envNameNode.from,
  38. to: envNameNode.to,
  39. line: state.doc.lineAt(envNameNode.from).number,
  40. }
  41. items.push(thisEnvironmentName)
  42. } else if (
  43. node.type.is('NewEnvironment') ||
  44. node.type.is('RenewEnvironment')
  45. ) {
  46. if (!nodeIntersectsChange(node.node)) {
  47. // This should already be in `items`
  48. return false
  49. }
  50. const envNameNode = node.node.getChild('LiteralArgContent')
  51. if (!envNameNode) {
  52. return
  53. }
  54. const envNameText = state.doc.sliceString(envNameNode.from, envNameNode.to)
  55. if (!envNameText) {
  56. return
  57. }
  58. const thisEnvironmentName = {
  59. title: envNameText,
  60. from: envNameNode.from,
  61. to: envNameNode.to,
  62. line: state.doc.lineAt(envNameNode.from).number,
  63. }
  64. items.push(thisEnvironmentName)
  65. }
  66. }
  67. export const cursorIsAtBeginEnvironment = (
  68. state: EditorState,
  69. pos: number
  70. ): boolean | undefined => {
  71. const tree = ensureSyntaxTree(state, pos, HUNDRED_MS)
  72. if (!tree) {
  73. return
  74. }
  75. let thisNode = tree.resolve(pos)
  76. if (!thisNode) {
  77. return
  78. }
  79. if (
  80. thisNode.type.is('EnvNameGroup') &&
  81. previousSiblingIs(state, pos, 'Begin')
  82. ) {
  83. return true
  84. } else if (
  85. thisNode.type.is('$Environment') ||
  86. (thisNode.type.is('LaTeX') && pos === state.doc.length) // We're at the end of the document
  87. ) {
  88. // We're at a malformed `\begin{`, resolve leftward
  89. thisNode = tree.resolve(pos, -1)
  90. if (!thisNode) {
  91. return
  92. }
  93. // TODO: may need to handle various envnames
  94. if (thisNode.type.is('OpenBrace') || thisNode.type.is('$EnvName')) {
  95. return true
  96. }
  97. }
  98. }
  99. export const cursorIsAtEndEnvironment = (
  100. state: EditorState,
  101. pos: number
  102. ): boolean | undefined => {
  103. const tree = ensureSyntaxTree(state, pos, HUNDRED_MS)
  104. if (!tree) {
  105. return
  106. }
  107. let thisNode = tree.resolve(pos)
  108. if (!thisNode) {
  109. return
  110. }
  111. if (
  112. thisNode.type.is('EnvNameGroup') &&
  113. previousSiblingIs(state, pos, 'End')
  114. ) {
  115. return true
  116. } else if (thisNode.type.is('$Environment') || thisNode.type.is('Content')) {
  117. // We're at a malformed `\end{`, resolve leftward
  118. thisNode = tree.resolve(pos, -1)
  119. if (!thisNode) {
  120. return
  121. }
  122. // TODO: may need to handle various envnames
  123. if (thisNode.type.is('OpenBrace') || thisNode.type.is('EnvName')) {
  124. return true
  125. }
  126. }
  127. }
  128. const findStartOfDocumentEnvironment = (tree: Tree): number | null => {
  129. const docEnvironment = findNodeInDocument(tree, 'DocumentEnvironment')
  130. return docEnvironment?.getChild('Content')?.from || null
  131. }
  132. const findStartOfAbstractEnvironment = (
  133. tree: Tree,
  134. state: EditorState
  135. ): number | null => {
  136. const abstractEnvironment = findNodeInDocument(
  137. tree,
  138. (nodeRef: SyntaxNodeRef) => {
  139. return Boolean(
  140. nodeRef.type.is('$Environment') &&
  141. getEnvironmentName(nodeRef.node, state) === 'abstract'
  142. )
  143. }
  144. )
  145. return abstractEnvironment?.getChild('Content')?.from || null
  146. }
  147. const findMaketitleCommand = (tree: Tree): number | null => {
  148. const maketitle = findNodeInDocument(tree, 'Maketitle')
  149. return maketitle?.to ?? null
  150. }
  151. const findNodeInDocument = (
  152. tree: Tree,
  153. predicate: number | string | ((node: SyntaxNodeRef) => boolean)
  154. ): SyntaxNode | null => {
  155. let node: SyntaxNode | null = null
  156. const predicateFn =
  157. typeof predicate !== 'function'
  158. ? (nodeRef: SyntaxNodeRef) => {
  159. return nodeRef.type.is(predicate)
  160. }
  161. : predicate
  162. tree?.iterate({
  163. enter(nodeRef) {
  164. if (node !== null) {
  165. return false
  166. }
  167. if (predicateFn(nodeRef)) {
  168. node = nodeRef.node
  169. return false
  170. }
  171. },
  172. })
  173. return node
  174. }
  175. export const findStartOfDocumentContent = (
  176. state: EditorState
  177. ): number | null => {
  178. const tree = ensureSyntaxTree(state, state.doc.length, HUNDRED_MS)
  179. if (!tree) {
  180. return null
  181. }
  182. return (
  183. findStartOfAbstractEnvironment(tree, state) ??
  184. findMaketitleCommand(tree) ??
  185. findStartOfDocumentEnvironment(tree)
  186. )
  187. }
  188. /**
  189. *
  190. * @param node A node of type `$Environment`, `BeginEnv`, or `EndEnv`
  191. * @param state The editor state to read the name from
  192. * @returns The editor name or null if a name cannot be found
  193. */
  194. export function getEnvironmentName(
  195. node: SyntaxNode | null,
  196. state: EditorState
  197. ): string | null {
  198. if (node?.type.is('$Environment')) {
  199. node = node.getChild('BeginEnv')
  200. }
  201. if (!node?.type.is('BeginEnv') && !node?.type.is('EndEnv')) {
  202. return null
  203. }
  204. const nameNode = node
  205. ?.getChild('EnvNameGroup')
  206. ?.getChild('OpenBrace')?.nextSibling
  207. if (!nameNode) {
  208. return null
  209. }
  210. // the name node is a parameter in the grammar, so we have no good way to
  211. // target the specific type
  212. if (nameNode.type.is('CloseBrace')) {
  213. return null
  214. }
  215. return state.sliceDoc(nameNode.from, nameNode.to)
  216. }
  217. export function getEnvironmentArguments(environmentNode: SyntaxNode) {
  218. return environmentNode.getChild('BeginEnv')?.getChildren('TextArgument')
  219. }