codemirror.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. import CodeMirror from "codemirror"
  2. import "codemirror-theme-github/theme/github.css"
  3. import "codemirror/theme/base16-dark.css"
  4. import "codemirror/theme/tomorrow-night-bright.css"
  5. import "codemirror/lib/codemirror.css"
  6. import "codemirror/addon/lint/lint.css"
  7. import "codemirror/addon/dialog/dialog.css"
  8. import "codemirror/addon/hint/show-hint.css"
  9. import "codemirror/addon/fold/foldgutter.css"
  10. import "codemirror/addon/fold/foldgutter"
  11. import "codemirror/addon/fold/brace-fold"
  12. import "codemirror/addon/fold/comment-fold"
  13. import "codemirror/addon/fold/indent-fold"
  14. import "codemirror/addon/display/autorefresh"
  15. import "codemirror/addon/lint/lint"
  16. import "codemirror/addon/hint/show-hint"
  17. import "codemirror/addon/display/placeholder"
  18. import "codemirror/addon/edit/closebrackets"
  19. import "codemirror/addon/search/search"
  20. import "codemirror/addon/search/searchcursor"
  21. import "codemirror/addon/search/jump-to-line"
  22. import "codemirror/addon/dialog/dialog"
  23. import "codemirror/addon/selection/active-line"
  24. import { watch, onMounted, ref, Ref, useContext } from "@nuxtjs/composition-api"
  25. import { LinterDefinition } from "./linting/linter"
  26. import { Completer } from "./completion"
  27. type CodeMirrorOptions = {
  28. extendedEditorConfig: Omit<CodeMirror.EditorConfiguration, "value">
  29. linter: LinterDefinition | null
  30. completer: Completer | null
  31. }
  32. const DEFAULT_EDITOR_CONFIG: CodeMirror.EditorConfiguration = {
  33. autoRefresh: true,
  34. lineNumbers: true,
  35. foldGutter: true,
  36. autoCloseBrackets: true,
  37. gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
  38. extraKeys: {
  39. "Ctrl-Space": "autocomplete",
  40. },
  41. viewportMargin: Infinity,
  42. styleActiveLine: true,
  43. }
  44. /**
  45. * A Vue composable to mount and use Codemirror
  46. *
  47. * NOTE: Make sure to import all the necessary Codemirror modules,
  48. * as this function doesn't import any other than the core
  49. * @param el Reference to the dom node to attach to
  50. * @param value Reference to value to read/write to
  51. * @param options CodeMirror options to pass
  52. */
  53. export function useCodemirror(
  54. el: Ref<any | null>,
  55. value: Ref<string>,
  56. options: CodeMirrorOptions
  57. ): { cm: Ref<CodeMirror.Position | null>; cursor: Ref<CodeMirror.Position> } {
  58. const { $colorMode } = useContext() as any
  59. const cm = ref<CodeMirror.Editor | null>(null)
  60. const cursor = ref<CodeMirror.Position>({ line: 0, ch: 0 })
  61. const updateEditorConfig = () => {
  62. Object.keys(options.extendedEditorConfig).forEach((key) => {
  63. // Only update options which need updating
  64. if (
  65. cm.value &&
  66. cm.value?.getOption(key as any) !==
  67. (options.extendedEditorConfig as any)[key]
  68. ) {
  69. cm.value?.setOption(
  70. key as any,
  71. (options.extendedEditorConfig as any)[key]
  72. )
  73. }
  74. })
  75. }
  76. const updateLinterConfig = () => {
  77. if (options.linter) {
  78. cm.value?.setOption("lint", options.linter)
  79. }
  80. }
  81. const updateCompleterConfig = () => {
  82. if (options.completer) {
  83. cm.value?.setOption("hintOptions", {
  84. completeSingle: false,
  85. hint: async (editor: CodeMirror.Editor) => {
  86. const pos = editor.getCursor()
  87. const text = editor.getValue()
  88. const token = editor.getTokenAt(pos)
  89. // It's not a word token, so, just increment to skip to next
  90. if (token.string.toUpperCase() === token.string.toLowerCase())
  91. token.start += 1
  92. const result = await options.completer!(text, pos)
  93. if (!result) return null
  94. return <CodeMirror.Hints>{
  95. from: { line: pos.line, ch: token.start },
  96. to: { line: pos.line, ch: token.end },
  97. list: result.completions
  98. .sort((a, b) => a.score - b.score)
  99. .map((x) => x.text),
  100. }
  101. },
  102. })
  103. }
  104. }
  105. const initialize = () => {
  106. if (!el.value) return
  107. cm.value = CodeMirror(el.value!, DEFAULT_EDITOR_CONFIG)
  108. cm.value.setValue(value.value)
  109. setTheme()
  110. updateEditorConfig()
  111. updateLinterConfig()
  112. updateCompleterConfig()
  113. cm.value.on("change", (instance) => {
  114. // External update propagation (via watchers) should be ignored
  115. if (instance.getValue() !== value.value) {
  116. value.value = instance.getValue()
  117. }
  118. })
  119. cm.value.on("cursorActivity", (instance) => {
  120. cursor.value = instance.getCursor()
  121. })
  122. }
  123. // Boot-up CodeMirror, set the value and listeners
  124. onMounted(() => {
  125. initialize()
  126. })
  127. // Reinitialize if the target ref updates
  128. watch(el, () => {
  129. if (cm.value) {
  130. const parent = cm.value.getWrapperElement()
  131. parent.remove()
  132. cm.value = null
  133. }
  134. initialize()
  135. })
  136. const setTheme = () => {
  137. if (cm.value) {
  138. cm.value?.setOption("theme", getThemeName($colorMode.value))
  139. }
  140. }
  141. const getThemeName = (mode: string) => {
  142. switch (mode) {
  143. case "system":
  144. return "default"
  145. case "light":
  146. return "github"
  147. case "dark":
  148. return "base16-dark"
  149. case "black":
  150. return "tomorrow-night-bright"
  151. default:
  152. return "default"
  153. }
  154. }
  155. // If the editor properties are reactive, watch for updates
  156. watch(() => options.extendedEditorConfig, updateEditorConfig, {
  157. immediate: true,
  158. deep: true,
  159. })
  160. watch(() => options.linter, updateLinterConfig, { immediate: true })
  161. watch(() => options.completer, updateCompleterConfig, { immediate: true })
  162. // Watch value updates
  163. watch(value, (newVal) => {
  164. // Check if we are mounted
  165. if (cm.value) {
  166. // Don't do anything on internal updates
  167. if (cm.value.getValue() !== newVal) {
  168. cm.value.setValue(newVal)
  169. }
  170. }
  171. })
  172. // Push cursor updates
  173. watch(cursor, (value) => {
  174. if (value !== cm.value?.getCursor()) {
  175. cm.value?.focus()
  176. cm.value?.setCursor(value)
  177. }
  178. })
  179. // Watch color mode updates and update theme
  180. watch(() => $colorMode.value, setTheme)
  181. return {
  182. cm,
  183. cursor,
  184. }
  185. }