annotations.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. import { EditorView, ViewUpdate } from '@codemirror/view'
  2. import { Diagnostic, linter, lintGutter } from '@codemirror/lint'
  3. import {
  4. Compartment,
  5. Extension,
  6. RangeSet,
  7. RangeValue,
  8. StateEffect,
  9. StateField,
  10. Text,
  11. } from '@codemirror/state'
  12. import { Annotation } from '../../../../../types/annotation'
  13. import { debugConsole } from '@/utils/debugging'
  14. import { sendMB } from '@/infrastructure/event-tracking'
  15. const compileLintSourceConf = new Compartment()
  16. export const annotations = () => [
  17. compileDiagnosticsState,
  18. compileLintSourceConf.of(compileLogLintSource()),
  19. /**
  20. * The built-in lint gutter extension, configured with zero hover delay.
  21. */
  22. lintGutter({
  23. hoverTime: 0,
  24. }),
  25. annotationsTheme,
  26. ]
  27. /**
  28. * A theme which moves the lint gutter outside the line numbers.
  29. */
  30. const annotationsTheme = EditorView.baseTheme({
  31. '.cm-gutter-lint': {
  32. order: -1,
  33. },
  34. })
  35. export const lintSourceConfig = {
  36. delay: 100,
  37. // Show highlights only for errors
  38. markerFilter(diagnostics: readonly Diagnostic[]) {
  39. return diagnostics.filter(d => d.severity === 'error')
  40. },
  41. // Do not show any tooltips for highlights within the editor content
  42. tooltipFilter() {
  43. return []
  44. },
  45. needsRefresh(update: ViewUpdate) {
  46. return update.selectionSet
  47. },
  48. }
  49. /**
  50. * A lint source using the compile log diagnostics
  51. */
  52. const compileLogLintSource = (): Extension =>
  53. linter(view => {
  54. const items: Diagnostic[] = []
  55. const cursor = view.state.field(compileDiagnosticsState).iter()
  56. while (cursor.value !== null) {
  57. const { diagnostic } = cursor.value
  58. items.push({
  59. ...diagnostic,
  60. from: cursor.from,
  61. to: cursor.to,
  62. renderMessage: () => renderMessage(diagnostic),
  63. })
  64. cursor.next()
  65. }
  66. return items
  67. }, lintSourceConfig)
  68. interface CompileLogDiagnostic extends Diagnostic {
  69. compile?: true
  70. ruleId?: string
  71. }
  72. class CompileLogDiagnosticRangeValue extends RangeValue {
  73. constructor(public diagnostic: CompileLogDiagnostic) {
  74. super()
  75. }
  76. }
  77. const setCompileDiagnosticsEffect = StateEffect.define<CompileLogDiagnostic[]>()
  78. /**
  79. * A state field for the compile log diagnostics
  80. */
  81. export const compileDiagnosticsState = StateField.define<
  82. RangeSet<CompileLogDiagnosticRangeValue>
  83. >({
  84. create() {
  85. return RangeSet.empty
  86. },
  87. update(value, transaction) {
  88. for (const effect of transaction.effects) {
  89. if (effect.is(setCompileDiagnosticsEffect)) {
  90. return RangeSet.of(
  91. effect.value.map(diagnostic =>
  92. new CompileLogDiagnosticRangeValue(diagnostic).range(
  93. diagnostic.from,
  94. diagnostic.to
  95. )
  96. ),
  97. true
  98. )
  99. }
  100. }
  101. if (transaction.docChanged) {
  102. value = value.map(transaction.changes)
  103. }
  104. return value
  105. },
  106. })
  107. export const setAnnotations = (doc: Text, annotations: Annotation[]) => {
  108. const diagnostics: Diagnostic[] = []
  109. for (const annotation of annotations) {
  110. // ignore "whole document" (row: -1) annotations
  111. if (annotation.row !== -1) {
  112. try {
  113. diagnostics.push(convertAnnotationToDiagnostic(doc, annotation))
  114. } catch (error) {
  115. // ignore invalid annotations
  116. debugConsole.debug('invalid annotation position', error)
  117. }
  118. }
  119. }
  120. return {
  121. effects: setCompileDiagnosticsEffect.of(diagnostics),
  122. }
  123. }
  124. export const showCompileLogDiagnostics = (show: boolean) => {
  125. return {
  126. effects: [
  127. // reconfigure the compile log lint source
  128. compileLintSourceConf.reconfigure(show ? compileLogLintSource() : []),
  129. ],
  130. }
  131. }
  132. const convertAnnotationToDiagnostic = (
  133. doc: Text,
  134. annotation: Annotation
  135. ): CompileLogDiagnostic => {
  136. if (annotation.row < 0) {
  137. throw new Error(`Invalid annotation row ${annotation.row}`)
  138. }
  139. const line = doc.line(annotation.row + 1)
  140. return {
  141. from: line.from,
  142. to: line.to, // NOTE: highlight whole line as synctex doesn't output column number
  143. severity: annotation.type,
  144. message: annotation.text,
  145. ruleId: annotation.ruleId,
  146. compile: true,
  147. }
  148. }
  149. export const renderMessage = (
  150. diagnostic: Pick<
  151. CompileLogDiagnostic,
  152. 'message' | 'severity' | 'ruleId' | 'compile'
  153. >
  154. ) => {
  155. const { message, severity, ruleId, compile = false } = diagnostic
  156. const div = document.createElement('div')
  157. div.textContent = message
  158. window.setTimeout(() => {
  159. if (div.isConnected) {
  160. sendMB('lint-gutter-marker-view', { severity, ruleId, compile })
  161. }
  162. }, 500) // 500ms delay to indicate intention, rather than accidental hover
  163. return div
  164. }