SqlEditorByAce.tsx 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. /*
  2. * <<
  3. * Davinci
  4. * ==
  5. * Copyright (C) 2016 - 2017 EDP
  6. * ==
  7. * Licensed under the Apache License, Version 2.0 (the "License");
  8. * you may not use this file except in compliance with the License.
  9. * You may obtain a copy of the License at
  10. *
  11. * http://www.apache.org/licenses/LICENSE-2.0
  12. *
  13. * Unless required by applicable law or agreed to in writing, software
  14. * distributed under the License is distributed on an "AS IS" BASIS,
  15. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16. * See the License for the specific language governing permissions and
  17. * limitations under the License.
  18. * >>
  19. */
  20. import React, { useRef, useEffect, useCallback, useMemo } from 'react'
  21. import AceEditor, { IAceOptions } from 'react-ace'
  22. import languageTools from 'ace-builds/src-min-noconflict/ext-language_tools'
  23. import 'ace-builds/src-min-noconflict/ext-searchbox'
  24. import 'ace-builds/src-min-noconflict/theme-sqlserver'
  25. import 'ace-builds/src-min-noconflict/mode-sql'
  26. import ReactAce, { IAceEditorProps } from 'react-ace/lib/ace'
  27. import { debounce } from 'lodash'
  28. import { DEFAULT_FONT_SIZE } from 'app/globalConstants'
  29. import Styles from '../View.less'
  30. type TMode =
  31. | 'sql'
  32. | 'mysql'
  33. | 'sqlserver'
  34. type TTheme =
  35. | 'ambiance'
  36. | 'chaos'
  37. | 'chrome'
  38. | 'clouds'
  39. | 'dawn'
  40. | 'eclipse'
  41. | 'github'
  42. | 'kuroir'
  43. | 'terminal'
  44. | 'textmate'
  45. | 'tomorrow'
  46. | 'twilight'
  47. | 'xcode'
  48. | 'sqlserver'
  49. enum EHintMeta {
  50. table = 'table',
  51. variable = 'variable',
  52. column = 'column'
  53. }
  54. const THEME_DEFAULT = 'sqlserver'
  55. const MODE_DEFAULT = 'sql'
  56. const EDITOR_OPTIONS: IAceOptions = {
  57. behavioursEnabled: true,
  58. enableSnippets: false,
  59. enableBasicAutocompletion: true,
  60. enableLiveAutocompletion: true,
  61. autoScrollEditorIntoView: true,
  62. wrap: true,
  63. useWorker: false
  64. }
  65. export interface ISqlEditorProps {
  66. hints: { [name: string]: string[] }
  67. value: string
  68. /**
  69. * 需引入对应的包 'ace-builds/src-min-noconflict/mode-${mode}'
  70. */
  71. mode?: TMode
  72. /**
  73. * 需引入对应的包 'ace-builds/src-min-noconflict/theme-${theme}'
  74. */
  75. theme?: TTheme
  76. editorConfig?: IAceEditorProps
  77. sizeChanged?: number
  78. onSqlChange: (sql: string) => void
  79. onSelect?: (sql: string) => void
  80. onCmdEnter?: () => void
  81. }
  82. /**
  83. * Editor Component
  84. * @param props ISqlEditorProps
  85. */
  86. function SqlEditor (props: ISqlEditorProps) {
  87. const refEditor = useRef<ReactAce>()
  88. const {
  89. hints,
  90. value,
  91. mode = MODE_DEFAULT,
  92. theme = THEME_DEFAULT,
  93. sizeChanged,
  94. editorConfig,
  95. onSqlChange,
  96. onSelect,
  97. onCmdEnter
  98. } = props
  99. const resize = useCallback(debounce(() => {
  100. refEditor.current.editor.resize()
  101. }, 300), [])
  102. const change = useCallback((sql: string) => {
  103. onSqlChange(sql)
  104. }, [])
  105. const selectionChange = useCallback(debounce((selection: any) => {
  106. const rawSelectedQueryText = refEditor.current.editor.session.doc.getTextRange(selection.getRange())
  107. const selectedQueryText = rawSelectedQueryText.length > 1 ? rawSelectedQueryText : null
  108. onSelect?.(selectedQueryText)
  109. }, 300), [])
  110. const commands = useMemo(() => [
  111. {
  112. name: 'execute',
  113. bindKey: { win: 'Ctrl-Enter', mac: 'Command-Enter' },
  114. exec: onCmdEnter
  115. }
  116. ], [])
  117. useEffect(() => {
  118. resize()
  119. }, [sizeChanged])
  120. useEffect(() => {
  121. setHintsPopover(hints)
  122. }, [hints])
  123. return (
  124. <div className={Styles.sqlEditor}>
  125. <AceEditor
  126. ref={refEditor}
  127. name="aceEditor"
  128. width="100%"
  129. height="100%"
  130. fontSize={DEFAULT_FONT_SIZE}
  131. mode={mode}
  132. theme={theme}
  133. value={value}
  134. showPrintMargin={false}
  135. highlightActiveLine={true}
  136. setOptions={EDITOR_OPTIONS}
  137. commands={commands}
  138. onChange={change}
  139. onSelectionChange={selectionChange}
  140. {...editorConfig}
  141. />
  142. </div>
  143. )
  144. }
  145. interface ICompleters {
  146. value: string
  147. name?: string
  148. caption?: string
  149. meta?: string
  150. type?: string
  151. score?: number
  152. }
  153. function setHintsPopover (hints: ISqlEditorProps['hints']) {
  154. const {
  155. textCompleter,
  156. keyWordCompleter,
  157. // snippetCompleter,
  158. setCompleters
  159. } = languageTools
  160. const customHintsCompleter = {
  161. identifierRegexps: [/[a-zA-Z_0-9.\-\u00A2-\uFFFF]/],
  162. getCompletions: (editor, session, pos, prefix, callback) => {
  163. const { tableKeywords, tableColumnKeywords, variableKeywords, columns } = formatCompleterFromHints(hints)
  164. if (prefix[prefix.length - 1] === '.') {
  165. const tableName = prefix.substring(0, prefix.length - 1)
  166. const AliasTableColumnKeywords = genAliasTableColumnKeywords(editor, tableName, hints)
  167. const hintList = tableKeywords.concat(variableKeywords, AliasTableColumnKeywords, tableColumnKeywords[tableName] || [])
  168. return callback(null, hintList)
  169. }
  170. callback(null, tableKeywords.concat(variableKeywords, columns))
  171. }
  172. }
  173. const completers = [
  174. textCompleter,
  175. keyWordCompleter,
  176. // snippetCompleter,
  177. customHintsCompleter
  178. ]
  179. setCompleters(completers)
  180. }
  181. function formatCompleterFromHints (hints: ISqlEditorProps['hints']) {
  182. const variableKeywords: ICompleters[] = []
  183. const tableKeywords: ICompleters[] = []
  184. const tableColumnKeywords: { [tableName: string]: ICompleters[] } = {}
  185. const columns: ICompleters[] = []
  186. let score = 1000
  187. Object.keys(hints).forEach((key) => {
  188. const meta: EHintMeta = isVariable(key)
  189. if (!meta) {
  190. const { columnWithTableName, column } = genTableColumnKeywords(hints[key], key)
  191. tableColumnKeywords[key] = columnWithTableName
  192. columns.push(...column)
  193. tableKeywords.push({ name: key, value: key, score: score--, meta: isTable() })
  194. } else {
  195. variableKeywords.push({ score: score--, value: key, meta })
  196. }
  197. })
  198. return { tableKeywords, tableColumnKeywords, variableKeywords, columns }
  199. }
  200. function genTableColumnKeywords (table: string[], tableName: string) {
  201. let score = 100
  202. const columnWithTableName: ICompleters[] = []
  203. const column: ICompleters[] = []
  204. table.forEach((columnVal) => {
  205. const basis = { score: score--, meta: isColumn() }
  206. columnWithTableName.push({
  207. caption: `${tableName}.${columnVal}`,
  208. name: `${tableName}.${columnVal}`,
  209. value: `${tableName}.${columnVal}`,
  210. ...basis
  211. })
  212. column.push({ value: columnVal, name: columnVal, ...basis })
  213. })
  214. return { columnWithTableName, column }
  215. }
  216. function genAliasTableColumnKeywords (editor, aliasTableName: string, hints: ISqlEditorProps['hints']) {
  217. const content = editor.getSession().getValue()
  218. const tableName = Object.keys(hints).find((tableName) => {
  219. const reg = new RegExp(`.+${tableName}\\s*(as|AS)?(?=\\s+${aliasTableName}\\s*)`, 'im')
  220. return reg.test(content)
  221. })
  222. if (!tableName) { return [] }
  223. const { columnWithTableName } = genTableColumnKeywords(hints[tableName], aliasTableName)
  224. return columnWithTableName
  225. }
  226. function isVariable (key: string) {
  227. return key.startsWith('$') && key.endsWith('$') && EHintMeta.variable
  228. }
  229. function isTable (key?: string) {
  230. return EHintMeta.table
  231. }
  232. function isColumn (key?: string) {
  233. return EHintMeta.column
  234. }
  235. export default SqlEditor