# react-codemirror2 使用小结

# 获取选中数据

// 获取选中数据 cm 为 codemirror 实例
getSelection: (cm) => {
  return cm && cm.doc.getSelection();
}
1
2
3
4

# 获取光标当前行数据

// 获取光标当前行数据
getCursorLineData: (cm) => {
  const { doc } = cm;
  const cursor = doc?.getCursor();
  return doc && doc.getLine(cursor.line);
}
1
2
3
4
5
6

# 获取当前光标前数据

// 获取当前光标前数据
getBeforeCursorLineData: (cm) => {
  const { doc } = cm;
  const cursor = doc?.getCursor();
  const token = cm.getTokenAt(cursor);
  const value = doc.getRange({ line: 0, ch: 0 }, { line: cursor.line, ch: token.end });

  return value;
}
1
2
3
4
5
6
7
8
9

# 关键字 或 表 数据 联想后 无法使用方向键选中

const autoComplete = (editor, event) => {
  const { key, keyCode } = event;
  // 上下方向键选择hint 直接返回 解决选中不了问题
  if (key === 'ArrowDown' || key === 'ArrowUp') return;
  // ...
};
1
2
3
4
5
6

# 获取光标所在行 及 以后的数据

// 获取当前光标所在行 及 以后的数据
getAfterCursorLineData: (cm) => {
  const { doc } = cm;
  const cursor = doc?.getCursor();
  // 最后一行
  const lastLine = doc.lastLine();
  // 获取最后一行数据
  const lastLineData = doc.getLine(lastLine) || '';
  const token = cm.getTokenAt(cursor);
  const value =
    // 关键 ch: 0
    doc.getRange({ line: cursor.line, ch: 0 }, { line: lastLine, ch: lastLineData.length || 0 });

  return value;
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 实际使用中,涉及多个 TabPane,每个 Pane 下都有一个 CodeMirror,需要维护每个 CodeMirror 的实例,用于父组件方法调用

  • 父组件中 key 必须,且不能用 index ,因为场景涉及删除 TabPane,涉及 index 的变化,重绘导致 onSaveInstance 再次触发,改变 实例cm 时会造成不必要的 BUG
// 父组件
<CodeMirror
  key={o.key}
  // 为每一个 tab 保存对应的实例
  onSaveInstance={(cm) => handleCommonFieldChange(cm, o, index, 'cm')}
/>

// 子组件
<CodeMirror
  // ...
  editorDidMount={(editor) => {
    // 保存每一个实例 用于父组件 getSelection getCursorLineData
    onSaveInstance(editor);
  }}
/>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 使用 hintOptions 配置异步获取 async: true hint: handleHintShow ,会影响关键字的联想

  • handleHintShow 参见下方源码

  • 解决方法: 使用 tables ,通过父组件传入,根据当前输入 SQL ,调用 onLoadColumns 改变 tables

<CodeMirror
  // ...
  options={{
    // ...
    hintOptions: {
      // 自定义提示选项
      completeSingle: false, // 当匹配只有一项的时候是否自动补全,建议false
      hint: handleHintShow,
      async: true,
    },
  }}
/>
1
2
3
4
5
6
7
8
9
10
11
12

# 源码

<CodeMirror
  // key 必须 不能使用 index 删除tab 时 index 改变会触发 handleCommonFieldChange(cm, o, index, 'cm') 导致删除tab时cm改变
  // 因为 删除(remove)时 会 setTabs , handleCommonFieldChange(cm, o, index, 'cm') 也会 setTabs 会导致数据不同步问题
  key={o.key}
  ref={refCodeMirror}
  text={o.fileContent}
  tables={backupDBList}
  onChange={(v) => handleCommonFieldChange(v, o, index, 'fileContent')}
  // 为每一个 tab 保存对应的实例
  onSaveInstance={(cm) => handleCommonFieldChange(cm, o, index, 'cm')}
  onLoadColumns={handleLoadColumns}
/>
1
2
3
4
5
6
7
8
9
10
11
12
import React, { useImperativeHandle, forwardRef, useState, useRef, useEffect } from 'react';
import styles from './index.module.scss';
import { UnControlled as CodeMirror } from 'react-codemirror2';
import 'codemirror/lib/codemirror.css';
import 'codemirror/lib/codemirror.js';
import 'codemirror/theme/eclipse.css';
import 'codemirror/mode/sql/sql.js'; // 用于高亮sql语句
import 'codemirror/addon/hint/show-hint';
import 'codemirror/addon/hint/show-hint.css';
import 'codemirror/addon/hint/sql-hint';
import 'codemirror/addon/display/placeholder.js';
// 用于生成placeholder
// require('codemirror/addon/hint/sql-hint');

// 高亮行功能
import 'codemirror/addon/selection/active-line';
import 'codemirror/addon/selection/selection-pointer';

const cm = require('codemirror/lib/codemirror.js');

interface SqlCodemirrorProps {
  text: any;
  onChange?: any;
  tables?: any;
  onLoadColumns?: any;
  onSaveInstance?: any;
}
interface SqlCodemirrorRef {
  getText: () => any;
  setText: (v: any) => any;
  replaceSelection: (cm: any, v: any) => any;
  getSelection: (v: any) => any;
  getCursorLineData: (v: any) => any;
  getBeforeCursorLineData: (v: any) => any;
  getAfterCursorLineData: (v: any) => any;
}

const ReactCodeMirror = (props, ref: React.Ref<SqlCodemirrorRef>) => {
  const { tables, onChange, onLoadColumns, onSaveInstance } = props;
  const [text, setText] = useState<string>('');
  const [tableKeys, setTableKeys] = useState<string[]>([]);
  const instance = useRef<any>(null);

  useEffect(() => {
    const keys = Object.keys(tables);
    setTableKeys(keys);
  }, [tables]);

  useImperativeHandle(ref, () => ({
    getText: () => {
      return text;
    },
    setText: (v) => {
      setText(v);
    },
    // 光标处设置值
    replaceSelection: (cm, v) => {
      cm && cm.replaceSelection(v);
    },
    // 获取选中数据
    getSelection: (cm) => {
      return cm && cm.doc.getSelection();
    },
    // 获取光标当前行数据
    getCursorLineData: (cm) => {
      const { doc } = cm;
      const cursor = doc?.getCursor();
      return doc && doc.getLine(cursor.line);
    },
    // 获取当前光标前数据
    getBeforeCursorLineData: (cm) => {
      const { doc } = cm;
      const cursor = doc?.getCursor();
      const token = cm.getTokenAt(cursor);
      const value = doc.getRange({ line: 0, ch: 0 }, { line: cursor.line, ch: token.end });

      return value;
    },
    // 获取当前光标所在行 及 以后的数据
    getAfterCursorLineData: (cm) => {
      const { doc } = cm;
      const cursor = doc?.getCursor();
      // 最后一行
      const lastLine = doc.lastLine();
      // 获取最后一行数据
      const lastLineData = doc.getLine(lastLine) || '';
      const token = cm.getTokenAt(cursor);
      const value =
        doc.getRange({ line: cursor.line, ch: 0 }, { line: lastLine, ch: lastLineData.length || 0 });

      return value;
    },
  }));


  const handleHintShow = (cmInstance, options) => {
    console.log(cmInstance, 12);

    let mode = cmInstance.doc.modeOption;
    if (mode === 'sql') mode = 'text/x-sql';
    // const editor = cmInstance.getDoc().getEditor();
    const { keywords } = cm.resolveMode(mode);
    const keys = Object.keys(keywords);
    console.log(keys, 'keywords');


    const cursor = cmInstance.getCursor();
    const cursorLine = cmInstance.getLine(cursor.line);
    const end = cursor.ch;
    const start = end;

    const token = cmInstance.getTokenAt(cursor);

    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve({
          list: ['hello', 'world', ...keys],
          from: { ch: token.start, line: cursor.line },
          to: { ch: token.end, line: cursor.line },
        });
      }, 300);
    });
  };

  const autoComplete = (editor, event) => {
    const { key, keyCode } = event;
    // 上下方向键选择hint 直接返回 解决选中不了问题
    if (key === 'ArrowDown' || key === 'ArrowUp') return;
    const inputVal = editor.getValue().toLowerCase();

    // 对codemirror关键词提示的优化
    if (
      (key &&
      keyCode !== 13 &&
      key !== ' ' &&
      key !== ';' &&
      keyCode !== 8 &&
      !editor.getOption('readOnly'))
    ) {
      editor.showHint();
    }

    const fined = tableKeys.find((o) => inputVal.includes(o.toLowerCase()));
    fined && !tables[fined]?.length && onLoadColumns(fined);
  };

  // 获取选中数据
  const getSelection = () => {
    return instance.current.doc.getSelection();
  };

  // 获取光标当前行数据
  const getCursorLineData = () => {
    const { doc } = instance.current;
    const cursor = doc.getCursor();
    return doc.getLine(cursor.line);
  };

  return (
    <div className={styles.querySql}>

      {/* <ServButton onClick={getSelection}>获取选中</ServButton>
      <ServButton onClick={getCursorLineData}>获取光标行</ServButton> */}
      <CodeMirror
        value={text}
        onChange={(editor, data, v) => {
          onChange(v);
        }}
        onKeyUp={(editor: any, event: any) => {
          autoComplete(editor, event);
        }}
        // onCursor={(editor, data) => {
        //   console.log(data, 'onCursor');
        // }}
        // onSelection={(editor, data) => {
        //   console.log(editor.doc.getSelection(), 'onSelection');
        // }}
        editorDidMount={(editor) => {
          // 保存每一个实例 用于父组件 getSelection getCursorLineData
          onSaveInstance(editor);
          instance.current = editor;
        }}
        options={{
          mode: 'text/x-sql', // 代码类型
          theme: 'eclipse', // 主题
          indentWithTabs: true, // 在缩进时,是否需要把 n*tab宽度个空格替换成n个tab字符,默认为false 。
          smartIndent: true, // 自动缩进,回车时跟上一行保持相同缩进
          lineNumbers: true, // 左侧显示行数
          matchBrackets: true, // 括号匹配
          cursorHeight: 1, // 光标高度
          autoRefresh: true,
          line: true,
          placeholder: props.placeholder || '请输入SQL脚本',
          // readOnly: true, // 是否只读
          hintOptions: {
            // 自定义提示选项
            completeSingle: false, // 当匹配只有一项的时候是否自动补全,建议false
            // hint: handleHintShow,
            // async: true,
            // disableKeywords: ['hello'],
            tables,
          },
          extraKeys: {
            // 触发按键
            Ctrl: 'autocomplete',
          },
        }}
      />
    </div>
  );
};

export default forwardRef<SqlCodemirrorRef, SqlCodemirrorProps>(ReactCodeMirror);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213