/* * << * Davinci * == * Copyright (C) 2016 - 2017 EDP * == * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * >> */ import React, { useRef, useEffect, useCallback, useMemo } from 'react' import AceEditor, { IAceOptions } from 'react-ace' import languageTools from 'ace-builds/src-min-noconflict/ext-language_tools' import 'ace-builds/src-min-noconflict/ext-searchbox' import 'ace-builds/src-min-noconflict/theme-sqlserver' import 'ace-builds/src-min-noconflict/mode-sql' import ReactAce, { IAceEditorProps } from 'react-ace/lib/ace' import { debounce } from 'lodash' import { DEFAULT_FONT_SIZE } from 'app/globalConstants' import Styles from '../View.less' type TMode = | 'sql' | 'mysql' | 'sqlserver' type TTheme = | 'ambiance' | 'chaos' | 'chrome' | 'clouds' | 'dawn' | 'eclipse' | 'github' | 'kuroir' | 'terminal' | 'textmate' | 'tomorrow' | 'twilight' | 'xcode' | 'sqlserver' enum EHintMeta { table = 'table', variable = 'variable', column = 'column' } const THEME_DEFAULT = 'sqlserver' const MODE_DEFAULT = 'sql' const EDITOR_OPTIONS: IAceOptions = { behavioursEnabled: true, enableSnippets: false, enableBasicAutocompletion: true, enableLiveAutocompletion: true, autoScrollEditorIntoView: true, wrap: true, useWorker: false } export interface ISqlEditorProps { hints: { [name: string]: string[] } value: string /** * 需引入对应的包 'ace-builds/src-min-noconflict/mode-${mode}' */ mode?: TMode /** * 需引入对应的包 'ace-builds/src-min-noconflict/theme-${theme}' */ theme?: TTheme editorConfig?: IAceEditorProps sizeChanged?: number onSqlChange: (sql: string) => void onSelect?: (sql: string) => void onCmdEnter?: () => void } /** * Editor Component * @param props ISqlEditorProps */ function SqlEditor (props: ISqlEditorProps) { const refEditor = useRef() const { hints, value, mode = MODE_DEFAULT, theme = THEME_DEFAULT, sizeChanged, editorConfig, onSqlChange, onSelect, onCmdEnter } = props const resize = useCallback(debounce(() => { refEditor.current.editor.resize() }, 300), []) const change = useCallback((sql: string) => { onSqlChange(sql) }, []) const selectionChange = useCallback(debounce((selection: any) => { const rawSelectedQueryText = refEditor.current.editor.session.doc.getTextRange(selection.getRange()) const selectedQueryText = rawSelectedQueryText.length > 1 ? rawSelectedQueryText : null onSelect?.(selectedQueryText) }, 300), []) const commands = useMemo(() => [ { name: 'execute', bindKey: { win: 'Ctrl-Enter', mac: 'Command-Enter' }, exec: onCmdEnter } ], []) useEffect(() => { resize() }, [sizeChanged]) useEffect(() => { setHintsPopover(hints) }, [hints]) return (
) } interface ICompleters { value: string name?: string caption?: string meta?: string type?: string score?: number } function setHintsPopover (hints: ISqlEditorProps['hints']) { const { textCompleter, keyWordCompleter, // snippetCompleter, setCompleters } = languageTools const customHintsCompleter = { identifierRegexps: [/[a-zA-Z_0-9.\-\u00A2-\uFFFF]/], getCompletions: (editor, session, pos, prefix, callback) => { const { tableKeywords, tableColumnKeywords, variableKeywords, columns } = formatCompleterFromHints(hints) if (prefix[prefix.length - 1] === '.') { const tableName = prefix.substring(0, prefix.length - 1) const AliasTableColumnKeywords = genAliasTableColumnKeywords(editor, tableName, hints) const hintList = tableKeywords.concat(variableKeywords, AliasTableColumnKeywords, tableColumnKeywords[tableName] || []) return callback(null, hintList) } callback(null, tableKeywords.concat(variableKeywords, columns)) } } const completers = [ textCompleter, keyWordCompleter, // snippetCompleter, customHintsCompleter ] setCompleters(completers) } function formatCompleterFromHints (hints: ISqlEditorProps['hints']) { const variableKeywords: ICompleters[] = [] const tableKeywords: ICompleters[] = [] const tableColumnKeywords: { [tableName: string]: ICompleters[] } = {} const columns: ICompleters[] = [] let score = 1000 Object.keys(hints).forEach((key) => { const meta: EHintMeta = isVariable(key) if (!meta) { const { columnWithTableName, column } = genTableColumnKeywords(hints[key], key) tableColumnKeywords[key] = columnWithTableName columns.push(...column) tableKeywords.push({ name: key, value: key, score: score--, meta: isTable() }) } else { variableKeywords.push({ score: score--, value: key, meta }) } }) return { tableKeywords, tableColumnKeywords, variableKeywords, columns } } function genTableColumnKeywords (table: string[], tableName: string) { let score = 100 const columnWithTableName: ICompleters[] = [] const column: ICompleters[] = [] table.forEach((columnVal) => { const basis = { score: score--, meta: isColumn() } columnWithTableName.push({ caption: `${tableName}.${columnVal}`, name: `${tableName}.${columnVal}`, value: `${tableName}.${columnVal}`, ...basis }) column.push({ value: columnVal, name: columnVal, ...basis }) }) return { columnWithTableName, column } } function genAliasTableColumnKeywords (editor, aliasTableName: string, hints: ISqlEditorProps['hints']) { const content = editor.getSession().getValue() const tableName = Object.keys(hints).find((tableName) => { const reg = new RegExp(`.+${tableName}\\s*(as|AS)?(?=\\s+${aliasTableName}\\s*)`, 'im') return reg.test(content) }) if (!tableName) { return [] } const { columnWithTableName } = genTableColumnKeywords(hints[tableName], aliasTableName) return columnWithTableName } function isVariable (key: string) { return key.startsWith('$') && key.endsWith('$') && EHintMeta.variable } function isTable (key?: string) { return EHintMeta.table } function isColumn (key?: string) { return EHintMeta.column } export default SqlEditor