use-codemirror.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. import {
  2. EditorState,
  3. Extension,
  4. Annotation,
  5. StateEffect,
  6. } from '@codemirror/state';
  7. import { EditorView } from '@codemirror/view';
  8. import { useState, useEffect, useRef } from 'react';
  9. import {
  10. highlightSpecialChars,
  11. drawSelection,
  12. dropCursor,
  13. rectangularSelection,
  14. crosshairCursor,
  15. highlightActiveLine,
  16. keymap,
  17. } from '@codemirror/view';
  18. export { EditorView } from '@codemirror/view';
  19. import {
  20. foldGutter,
  21. indentOnInput,
  22. syntaxHighlighting,
  23. defaultHighlightStyle,
  24. bracketMatching,
  25. foldKeymap,
  26. } from '@codemirror/language';
  27. import { history, defaultKeymap, historyKeymap } from '@codemirror/commands';
  28. import { highlightSelectionMatches, searchKeymap } from '@codemirror/search';
  29. import {
  30. closeBrackets,
  31. autocompletion,
  32. closeBracketsKeymap,
  33. completionKeymap,
  34. } from '@codemirror/autocomplete';
  35. import { lintKeymap } from '@codemirror/lint';
  36. import {
  37. DEFAULT_CODE_VALUE,
  38. DEFAULT_FOLD_LINE_RANGES,
  39. } from '@/pages/play/Constants';
  40. import {
  41. lineNumbers,
  42. highlightActiveLineGutter,
  43. } from '../language/extensions/gutter';
  44. import {
  45. foldByLineRanges,
  46. loadFoldState,
  47. recoveryFoldState,
  48. } from '../language/extensions/fold';
  49. const basicSetup = () => [
  50. lineNumbers(),
  51. highlightActiveLineGutter(),
  52. highlightSpecialChars(),
  53. history(),
  54. foldGutter(),
  55. drawSelection(),
  56. dropCursor(),
  57. EditorState.allowMultipleSelections.of(true),
  58. indentOnInput(),
  59. syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
  60. bracketMatching(),
  61. closeBrackets(),
  62. autocompletion(),
  63. rectangularSelection(),
  64. crosshairCursor(),
  65. highlightActiveLine(),
  66. highlightSelectionMatches(),
  67. keymap.of([
  68. ...closeBracketsKeymap,
  69. ...defaultKeymap,
  70. ...searchKeymap,
  71. ...historyKeymap,
  72. ...foldKeymap,
  73. ...completionKeymap,
  74. ...lintKeymap,
  75. ]),
  76. ];
  77. const External = Annotation.define<boolean>();
  78. export interface UseCodeMirrorProps {
  79. container?: HTMLDivElement | null;
  80. value?: string;
  81. extensions?: Extension[];
  82. onChange?: (value: string) => void;
  83. }
  84. export const useCodeMirror = (props: UseCodeMirrorProps) => {
  85. const { value, extensions, onChange } = props;
  86. const [container, setContainer] = useState<HTMLDivElement | null>();
  87. const viewRef = useRef<EditorView>();
  88. const updateListener = EditorView.updateListener.of(update => {
  89. if (update.docChanged) {
  90. onChange?.(update.state.doc.toString());
  91. }
  92. });
  93. const getExtensions = () => {
  94. return [updateListener, basicSetup(), ...(extensions ?? [])];
  95. };
  96. // init editor
  97. useEffect(() => {
  98. if (container && !viewRef.current) {
  99. const startState = EditorState.create({
  100. doc: value,
  101. extensions: getExtensions(),
  102. });
  103. const editorView = new EditorView({
  104. state: startState,
  105. parent: container,
  106. });
  107. const foldState = loadFoldState();
  108. if (foldState) {
  109. recoveryFoldState(editorView);
  110. } else if (value === DEFAULT_CODE_VALUE) {
  111. foldByLineRanges(editorView, DEFAULT_FOLD_LINE_RANGES);
  112. }
  113. viewRef.current = editorView;
  114. }
  115. return () => {
  116. viewRef.current?.destroy();
  117. viewRef.current = undefined;
  118. };
  119. }, [container]);
  120. // update value
  121. useEffect(() => {
  122. if (value === undefined) {
  123. return;
  124. }
  125. const view = viewRef.current;
  126. const currentValue = view?.state.doc.toString() ?? '';
  127. if (view && value !== currentValue) {
  128. view.dispatch({
  129. changes: {
  130. from: 0,
  131. to: currentValue.length,
  132. insert: value || '',
  133. },
  134. annotations: [External.of(true)],
  135. });
  136. }
  137. }, [value]);
  138. // update extensions
  139. useEffect(() => {
  140. const view = viewRef.current;
  141. if (view) {
  142. view.dispatch({
  143. effects: [StateEffect.reconfigure.of(getExtensions())],
  144. });
  145. }
  146. }, [extensions]);
  147. useEffect(() => setContainer(props.container), [props.container]);
  148. // Handle codelens shortcuts
  149. useEffect(() => {
  150. const handler = (event: Event) => {
  151. if (event instanceof KeyboardEvent) {
  152. if (event.metaKey && event.shiftKey && event.key === 'Enter') {
  153. const currentRunButton = document.querySelector(
  154. '.milvus-http-request-highlight .playground-codelens .run-button'
  155. );
  156. currentRunButton?.dispatchEvent(new MouseEvent('click'));
  157. event.preventDefault();
  158. } else if (event.metaKey && event.key === 'h') {
  159. const currentDocsButton = document.querySelector(
  160. '.milvus-http-request-highlight .playground-codelens .docs-button'
  161. );
  162. currentDocsButton?.dispatchEvent(new MouseEvent('click'));
  163. event.preventDefault();
  164. }
  165. }
  166. };
  167. container?.addEventListener('keydown', handler);
  168. return () => {
  169. container?.removeEventListener('keydown', handler);
  170. };
  171. }, [container]);
  172. return { view: viewRef.current };
  173. };