Upgrade CodeMirror from version 5 to 6. #7097

This commit is contained in:
Aditya Toshniwal 2024-02-21 11:15:25 +05:30 committed by GitHub
parent 721290b1e9
commit d3ede3151a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
53 changed files with 1565 additions and 1611 deletions

View File

@ -76,6 +76,7 @@
"dependencies": {
"@babel/plugin-proposal-class-properties": "^7.10.4",
"@babel/preset-react": "^7.12.13",
"@codemirror/lang-sql": "^6.5.5",
"@date-io/core": "^1.3.6",
"@date-io/date-fns": "1.x",
"@emotion/sheet": "^1.0.1",
@ -102,7 +103,7 @@
"chartjs-plugin-zoom": "^2.0.1",
"classnames": "^2.2.6",
"closest": "^0.0.1",
"codemirror": "^5.59.2",
"codemirror": "^6.0.1",
"convert-units": "^2.3.4",
"cssnano": "^5.0.2",
"dagre": "^0.8.4",
@ -174,6 +175,7 @@
"bundle": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=2048 yarn run bundle:dev",
"test:js-once": "yarn run linter && yarn run jest --maxWorkers=50%",
"test:js": "yarn run test:js-once --watch",
"test:js-file": "yarn run test:js-once -t",
"test:js-coverage": "yarn run test:js-once --collect-coverage",
"test:feature": "yarn run bundle && python regression/runtests.py --pkg feature_tests",
"test": "yarn run test:js-once && yarn run bundle && python regression/runtests.py",

View File

@ -1,6 +1,3 @@
div[role=tabpanel] > .pgadmin-control-group.form-group.c.jscexceptions {
min-height: 400px;
}
div[role=tabpanel] >.pgadmin-control-group.jstcode .CodeMirror-sizer{
height: 400px;
}

View File

@ -17,17 +17,14 @@ import usePreferences, { setupPreferenceBroadcast } from '../../../preferences/s
import checkNodeVisibility from '../../../static/js/check_node_visibility';
define('pgadmin.browser', [
'sources/gettext', 'sources/url_for', 'sources/pgadmin', 'bundled_codemirror',
'sources/gettext', 'sources/url_for', 'sources/pgadmin',
'sources/csrf', 'pgadmin.authenticate.kerberos',
'pgadmin.browser.utils', 'pgadmin.browser.messages',
'pgadmin.browser.node', 'pgadmin.browser.collection', 'pgadmin.browser.activity',
'sources/codemirror/addon/fold/pgadmin-sqlfoldcode',
'pgadmin.browser.keyboard', 'sources/tree/pgadmin_tree_save_state',
], function(
gettext, url_for, pgAdmin, codemirror, csrfToken, Kerberos,
gettext, url_for, pgAdmin, csrfToken, Kerberos,
) {
let CodeMirror = codemirror.default;
let pgBrowser = pgAdmin.Browser = pgAdmin.Browser || {};
let select_object_msg = gettext('Please select an object in the tree view.');
@ -1766,13 +1763,6 @@ define('pgadmin.browser', [
},
});
/* Remove paste event mapping from CodeMirror's emacsy KeyMap binding
* specific to Mac LineNumber:5797 - lib/Codemirror.js
* It is preventing default paste event(Cmd-V) from triggering
* in runtime.
*/
delete CodeMirror.keyMap.emacsy['Ctrl-V'];
// Use spaces instead of tab
if (pgBrowser.utils.useSpaces == 'True') {
pgAdmin.Browser.editor_shortcut_keys.Tab = 'insertSoftTab';

View File

@ -7,8 +7,7 @@
code,
kbd,
pre,
samp,
.CodeMirror pre {
samp {
font-family: $font-family-editor !important;
}

View File

@ -1,7 +1,7 @@
{% if file_name is not defined %}
{% set file_name=node_type %}
{% endif %}
.icon-{{file_name}} {
.icon-{{file_name}}, .cm-autocomplete-option-{{file_name}} .cm-completionIcon {
background-image: url('{{ url_for('NODE-%s.static' % node_type, filename='img/%s.svg' % file_name )}}') !important;
background-repeat: no-repeat;
background-size: 20px !important;

View File

@ -16,7 +16,7 @@ window.hookConsole = function(callback) {
}
try {
require(
['sources/generated/app.bundle', 'sources/generated/codemirror', 'sources/generated/browser_nodes'],
['sources/generated/app.bundle', 'sources/generated/browser_nodes'],
function() {
},
function() {

View File

@ -35,14 +35,6 @@
font-size: inherit;
}
#server_activity .CodeMirror,
#database_activity .CodeMirror,
#server_activity .CodeMirror-scroll,
#database_activity .CodeMirror-scroll {
height: auto;
max-height:100px;
}
.dashboard-hidden {
display: none;
}

View File

@ -13,7 +13,7 @@ import gettext from 'sources/gettext';
import PropTypes from 'prop-types';
import getApiInstance from 'sources/api_instance';
import { makeStyles } from '@material-ui/core/styles';
import CodeMirror from '../../../../static/js/components/CodeMirror';
import CodeMirror from '../../../../static/js/components/ReactCodeMirror';
import Loader from 'sources/components/Loader';
import withStandardTabInfo from '../../../../static/js/helpers/withStandardTabInfo';
import { BROWSER_PANELS } from '../../../../browser/static/js/constants';
@ -24,7 +24,6 @@ const useStyles = makeStyles((theme) => ({
height: '100% !important',
width: '100% !important',
background: theme.palette.grey[400],
overflow: 'auto !important',
minHeight: '100%',
minWidth: '100%',
},
@ -99,10 +98,7 @@ function SQL({nodeData, node, treeNodeInfo, isActive, isStale, setIsStale}) {
className={classes.textArea}
value={nodeSQL}
readonly={true}
options={{
lineNumbers: true,
mode: 'text/x-pgsql',
}}
showCopyBtn
/>
</>
);

View File

@ -1,58 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import CodeMirror from 'codemirror/lib/codemirror';
import 'codemirror/mode/sql/sql';
import 'codemirror/addon/selection/mark-selection';
import 'codemirror/addon/selection/active-line';
import 'codemirror/addon/fold/foldcode';
import 'codemirror/addon/fold/foldgutter';
import 'codemirror/addon/hint/show-hint';
import 'codemirror/addon/hint/sql-hint';
import 'codemirror/addon/scroll/simplescrollbars';
import 'codemirror/addon/dialog/dialog';
import 'codemirror/addon/search/search';
import 'codemirror/addon/search/searchcursor';
import 'codemirror/addon/search/jump-to-line';
import 'codemirror/addon/edit/matchbrackets';
import 'codemirror/addon/edit/closebrackets';
import 'codemirror/addon/comment/comment';
import 'sources/codemirror/addon/fold/pgadmin-sqlfoldcode';
import 'sources/codemirror/extension/centre_on_line';
let cmds = CodeMirror.commands;
cmds.focusOut = function(){
event.stopPropagation();
document.activeElement.blur();
if(event.currentTarget.hasOwnProperty('parents') && event.currentTarget.parents().find('.sql-code-control')) {
// for code mirror in dialogs
event.currentTarget.parents().find('.sql-code-control').focus();
}
};
CodeMirror.defineInitHook(function (codeMirror) {
codeMirror.addKeyMap({
Tab: function (cm) {
if(cm.somethingSelected()){
cm.execCommand('indentMore');
}
else {
if (cm.getOption('indentWithTabs')) {
cm.replaceSelection('\t', 'end', '+input');
}
else {
cm.execCommand('insertSoftTab');
}
}
},
});
});
CodeMirror.keyMap.default['Esc'] = 'focusOut';
export default CodeMirror;

View File

@ -1,10 +1,6 @@
@import 'node_modules/@fortawesome/fontawesome-free/css/all.css';
@import 'node_modules/leaflet/dist/leaflet.css';
@import 'node_modules/codemirror/lib/codemirror.css';
@import 'node_modules/codemirror/addon/dialog/dialog.css';
@import 'node_modules/codemirror/addon/scroll/simplescrollbars.css';
@import 'node_modules/xterm/css/xterm.css';
@import 'node_modules/jsoneditor/dist/jsoneditor.min.css';

View File

@ -111,7 +111,27 @@ export default function(basicSettings) {
diffColorFg: '#d4d4d4',
diffSelectFG: '#d4d4d4',
diffSelCheckbox: '#323E43'
}
},
editor: {
fg: '#fff',
bg: '#212121',
selectionBg: '#536270',
keyword: '#db7c74',
number: '#7fcc5c',
string: '#e4e487',
variable: '#7dc9f1',
type: '#7dc9f1',
comment: '#7fcc5c',
punctuation: '#d6aaaa',
operator: '#d6aaaa',
////
foldmarker: '#0000FF',
activeline: '#323e43',
activelineLight: '#323e43',
activelineBorderColor: 'none',
guttersBg: '#303030',
guttersFg: '#8A8A8A',
},
}
});
}

View File

@ -109,7 +109,27 @@ export default function(basicSettings) {
diffColorFg: '#FFFFFF',
diffSelectFG: '#010B15',
diffSelCheckbox: '#010b15',
}
},
editor: {
fg: '#fff',
bg: '#010B15',
selectionBg: '#1F2932',
keyword: '#F8845F',
number: '#45D48A',
string: '#EAEA43',
variable: '#7DC9F1',
type: '#7DC9F1',
comment: '#FFAD65',
punctuation: '#d6aaaa',
operator: '#d6aaaa',
////
foldmarker: '#FFFFFF',
activeline: '#063057',
activelineLight: '#063057',
activelineBorderColor: 'none',
guttersBg: '#2d3a48',
guttersFg: '#8b9cac',
},
}
});
}

View File

@ -23,6 +23,7 @@ import { CssBaseline } from '@material-ui/core';
import pickrOverride from './overrides/pickr.override';
import uplotOverride from './overrides/uplot.override';
import rcdockOverride from './overrides/rcdock.override';
import cmOverride from './overrides/codemirror.override';
/* Common settings across all themes */
let basicSettings = createTheme();
@ -333,6 +334,7 @@ function getFinalTheme(baseTheme) {
...pickrOverride(baseTheme),
...uplotOverride(baseTheme),
...rcdockOverride(baseTheme),
...cmOverride(baseTheme)
},
},
MuiOutlinedInput: {

View File

@ -0,0 +1,129 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2023, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
export default function cmOverride(theme) {
const editor = theme.otherVars.editor;
return {
'.cm-editor': {
height: '100%',
color: editor.fg,
backgroundColor: editor.bg,
'&.cm-focused': {
outline: 'none',
'& .cm-scroller > .cm-selectionLayer .cm-selectionBackground': {
background: editor.selectionBg,
}
},
'& .cm-scroller': {
...theme.mixins.fontSourceCode,
'& .cm-content': {
'&[aria-readonly="true"] + .cm-cursorLayer': {
display: 'none',
},
'& .cm-activeLine': {
backgroundColor: editor.activeline,
},
'& .cm-activeLineManual': {
backgroundColor: editor.activeline,
},
'& .tok-keyword': {
color: editor.keyword,
fontWeight: 600
},
'& .tok-number': {
color: editor.number,
fontWeight: 600
},
'& .tok-string': {color: editor.string},
'& .tok-variable': {color: editor.variable },
'& .tok-comment': {color: editor.comment},
'& .tok-operator': { color: editor.operator },
'& .tok-punctuation': {color: editor.punctuation},
'& .tok-typeName': {color: editor.type},
},
'& .cm-selectionLayer': {
'& .cm-selectionBackground': {
background: editor.selectionBg,
}
},
},
'& .cm-cursorLayer': {
'& .cm-cursor, & .cm-dropCursor': {
borderLeftColor: editor.fg,
}
},
'& .cm-gutters': {
backgroundColor: editor.guttersBg,
color: editor.guttersFg,
borderRight: 'none',
'& .cm-foldGutter': {
padding: '0px',
color: editor.fg,
},
'& .cm-breakpoint-gutter': {
padding: '0px 2px',
cursor: 'pointer',
'& .cm-gutterElement': {
fontSize: '1.3em',
lineHeight: '1.1',
color: 'red'
}
}
},
'& .cm-panels-bottom': {
border: '0 !important',
'& .cm-search': {
display: 'none',
}
},
'& .cm-error-highlight': {
borderBottom: '2px dotted red',
}
},
'.cm-tooltip': {
backgroundColor: theme.palette.background.default + '!important',
color: theme.palette.text.primary + '!important',
border: `1px solid ${theme.otherVars.borderColor} !important`,
'& li[aria-selected="true"]': {
backgroundColor: theme.otherVars.treeBgSelected + '!important',
color: theme.otherVars.treeFgSelected + '!important',
},
'& .pg-cm-autocomplete-icon': {
// marginRight: '2px',
marginLeft: '-2px',
padding: '0px 8px',
backgroundPosition: '50%',
width: '20px',
display: 'inline-block',
},
'&.pg-autocomp-loader': {
position: 'absolute',
paddingRight: '8px',
}
}
};
}

View File

@ -130,7 +130,29 @@ export default function(basicSettings) {
diffColorFg: '#222',
diffSelectFG: '#222',
diffSelCheckbox: '#d6effc'
}
},
editor: {
fg: '#222',
bg: '#fff',
selectionBg: '#d6effc',
keyword: '#908',
number: '#964',
string: '#a11',
variable: '#222',
type: '#05a',
comment: '#a50',
punctuation: '#737373',
operator: '#222',
////
foldmarker: '#0000FF',
activeline: '#EDF9FF',
activelineLight: '#EDF9FF',
activelineBorderColor: '#BCDEF3',
guttersBg: '#f3f5f9',
guttersFg: '#848ea0',
},
treeBgSelected: '#d6effc',
treeFgSelected: '#222',
}
});
}

View File

@ -1,131 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
(function(mod) {
if (typeof exports == 'object' && typeof module == 'object') // CommonJS
mod(require('codemirror'));
else if (typeof define == 'function' && define.amd) // AMD
define(['codemirror'], mod);
else // Plain browser env
mod(window.CodeMirror);
})(function(CodeMirror) {
'use strict';
let pgadminKeywordRangeFinder = function(cm, start, tokenSet) {
let line = start.line,
lineText = cm.getLine(line);
let at = lineText.length,
startChar, tokenType;
let tokenSetNo = 0,
startToken, endToken;
let startTkn = tokenSet[tokenSetNo].start,
endTkn = tokenSet[tokenSetNo].end;
while (at > 0) {
let found = lineText.toUpperCase().lastIndexOf(startTkn, at);
found = checkStartTokenFoundOnEndToken(found, lineText.toUpperCase(), endTkn, startTkn);
startToken = startTkn;
endToken = endTkn;
if (found < start.ch) {
/* If the start token is not found then search for the next set of token */
tokenSetNo++;
if(tokenSetNo >= tokenSet.length) {
return undefined;
}
startTkn = tokenSet[tokenSetNo].start;
endTkn = tokenSet[tokenSetNo].end;
at = lineText.length;
continue;
}
tokenType = cm.getTokenAt(CodeMirror.Pos(line, found + 1)).type;
if (!/^(comment|string)/.test(tokenType)) {
startChar = found;
break;
}
at = found - 1;
}
if (startChar == null || lineText.toUpperCase().lastIndexOf(startToken) > startChar) return;
let count = 1,
lastLine = cm.lineCount(),
end, endCh;
outer: for (let i = line + 1; i < lastLine; ++i) {
let text = cm.getLine(i).toUpperCase(),
pos = 0;
let whileloopvar = 0;
while (whileloopvar < 1) {
let nextOpen = text.indexOf(startToken, pos);
nextOpen = checkStartTokenFoundOnEndToken(nextOpen, text, endToken, startToken);
let nextClose = text.indexOf(endToken, pos);
if (nextOpen < 0) nextOpen = text.length;
if (nextClose < 0) nextClose = text.length;
pos = Math.min(nextOpen, nextClose);
if (pos == text.length) { whileloopvar=1; break; }
if (cm.getTokenAt(CodeMirror.Pos(i, pos + 1)).type == tokenType) {
if (pos == nextOpen) ++count;
else if (!--count) {
end = i;
endCh = pos;
break outer;
}
}
++pos;
}
}
if (end == null || end == line + 1) return;
return {
from: CodeMirror.Pos(line, startChar + startTkn.length),
to: CodeMirror.Pos(end, endCh),
};
};
/**
* This function is responsible for finding whether the startToken is present
* in the endToken as well, to avoid mismatch of start and end points.
* e.g. In case of IF and END IF, IF is detected in both tokens, which creates
* confusion. The said function will resolve such issues.
* @function checkStartTokenFoundOnEndToken
* @returns {Number} - returns found
*/
function checkStartTokenFoundOnEndToken(found, text, endToken, startToken) {
if(found > 0) {
if(text.includes(endToken)
|| !checkTokenMixedWithOtherAlphabets(text, startToken)) {
found = -1;
}
}
return found;
}
/**
* This function is responsible for finding whether the startToken is mixed
* with other alphabets of the text. To avoid word like NOTIFY to be mistakenly treat as keyword.
* e.g. to avoid the IF detected as keyword in the word pgAdmin.Browser.notifier.
* Function also works with other tokens like LOOP, CASE, etc.
* @function checkTokenMixedWithOtherAlphabets
* @returns {Boolean} - returns true/false
*/
function checkTokenMixedWithOtherAlphabets(text, startToken) {
//this reg will check the token should be in format as - IF condition or IF(condition)
let reg = `\\b\\${startToken}\\s*\\(\\w*\\)(?!\\w)|\\b\\${startToken}\\(\\w*\\)(?!\\w)|\\b\\${startToken}\\s*(?!\\w)`;
let regex = RegExp(reg, 'g');
return regex.exec(text) !== null;
}
CodeMirror.registerHelper('fold', 'sql', function(cm, start) {
return pgadminKeywordRangeFinder(cm, start, [
{start: 'BEGIN', end:'END;'},
{start: 'IF', end:'END IF'},
{start: 'LOOP', end:'END LOOP'},
{start: 'CASE', end:'END CASE'},
]);
});
});

View File

@ -1,16 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import CodeMirror from 'codemirror/lib/codemirror';
CodeMirror.defineExtension('centerOnLine', function(line) {
let ht = this.getScrollInfo().clientHeight;
let coords = this.charCoords({line: line, ch: 0}, 'local');
this.scrollTo(null, (coords.top + coords.bottom - ht) / 2);
});

View File

@ -1,587 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React, { useEffect, useMemo, useRef, useState } from 'react';
import OrigCodeMirror from 'bundled_codemirror';
import {useOnScreen} from 'sources/custom_hooks';
import PropTypes from 'prop-types';
import CustomPropTypes from '../custom_prop_types';
import gettext from 'sources/gettext';
import { Box, InputAdornment, makeStyles } from '@material-ui/core';
import clsx from 'clsx';
import { InputText } from './FormComponents';
import { DefaultButton, PgIconButton } from './Buttons';
import CloseIcon from '@material-ui/icons/CloseRounded';
import ArrowDownwardRoundedIcon from '@material-ui/icons/ArrowDownwardRounded';
import ArrowUpwardRoundedIcon from '@material-ui/icons/ArrowUpwardRounded';
import SwapHorizRoundedIcon from '@material-ui/icons/SwapHorizRounded';
import SwapCallsRoundedIcon from '@material-ui/icons/SwapCallsRounded';
import _ from 'lodash';
import { RegexIcon, FormatCaseIcon } from './ExternalIcon';
import { isMac } from '../keyboard_shortcuts';
import { checkTrojanSource } from '../utils';
import { copyToClipboard } from '../clipboard';
import { useDelayedCaller } from '../../../static/js/custom_hooks';
import usePreferences from '../../../preferences/static/js/store';
const useStyles = makeStyles((theme)=>({
root: {
position: 'relative',
},
hideCursor: {
'& .CodeMirror-cursors': {
display: 'none'
}
},
findDialog: {
position: 'absolute',
zIndex: 99,
right: '4px',
...theme.mixins.panelBorder.all,
borderTop: 'none',
padding: '2px 4px',
width: '250px',
backgroundColor: theme.palette.background.default,
},
marginTop: {
marginTop: '0.25rem',
},
copyButton: {
position: 'absolute',
zIndex: 99,
right: '4px',
width: '66px',
}
}));
function parseString(string) {
return string.replace(/\\([nrt\\])/g, function(match, ch) {
if (ch == 'n') return '\n';
if (ch == 'r') return '\r';
if (ch == 't') return '\t';
if (ch == '\\') return '\\';
return match;
});
}
function parseQuery(query, useRegex=false, matchCase=false) {
try {
if (useRegex) {
query = new RegExp(query, matchCase ? 'g': 'gi');
} else {
query = parseString(query);
if(!matchCase) {
query = query.toLowerCase();
}
}
if (typeof query == 'string' ? query == '' : query.test(''))
query = /x^/;
return query;
} catch (error) {
return null;
}
}
function getRegexFinder(query) {
return (stream) => {
query.lastIndex = stream.pos;
let match = query.exec(stream.string);
if (match && match.index == stream.pos) {
stream.pos += match[0].length || 1;
return 'searching';
} else if (match) {
stream.pos = match.index;
} else {
stream.skipToEnd();
}
};
}
function getPlainStringFinder(query, matchCase) {
return (stream) => {
let matchIndex = (matchCase ? stream.string : stream.string.toLowerCase()).indexOf(query, stream.pos);
if(matchIndex == -1) {
stream.skipToEnd();
} else if(matchIndex == stream.pos) {
stream.pos += query.length;
return 'searching';
} else {
stream.pos = matchIndex;
}
};
}
function searchOverlay(query, matchCase) {
return {
token: typeof query == 'string' ?
getPlainStringFinder(query, matchCase) : getRegexFinder(query)
};
}
export const CodeMirrorInstancType = PropTypes.shape({
getCursor: PropTypes.func,
getSearchCursor: PropTypes.func,
removeOverlay: PropTypes.func,
addOverlay: PropTypes.func,
setSelection: PropTypes.func,
scrollIntoView: PropTypes.func,
getSelection: PropTypes.func,
});
export function FindDialog({editor, show, replace, onClose, selFindVal}) {
const [findVal, setFindVal] = useState(selFindVal);
const [replaceVal, setReplaceVal] = useState('');
const [useRegex, setUseRegex] = useState(false);
const [matchCase, setMatchCase] = useState(false);
const findInputRef = useRef();
const highlightsearch = useRef();
const searchCursor = useRef();
const classes = useStyles();
const search = ()=>{
if(editor) {
let query = parseQuery(findVal, useRegex, matchCase);
if(!query) return;
searchCursor.current = editor.getSearchCursor(query, 0, !matchCase);
if(findVal != '') {
editor.removeOverlay(highlightsearch.current);
highlightsearch.current = searchOverlay(query, matchCase);
editor.addOverlay(highlightsearch.current);
onFindNext();
} else {
editor.removeOverlay(highlightsearch.current);
}
}
};
useEffect(()=>{
if(show) {
// Get selected text from editor and set it to find/replace input.
let selText = editor.getSelection();
if(selText.length != 0) {
setFindVal(selText);
}
findInputRef.current?.select();
search();
}
}, [show]);
useEffect(()=>{
search();
}, [findVal, useRegex, matchCase]);
const clearAndClose = ()=>{
editor.removeOverlay(highlightsearch.current);
onClose();
};
const toggle = (name)=>{
if(name == 'regex') {
setUseRegex((prev)=>!prev);
} else if(name == 'case') {
setMatchCase((prev)=>!prev);
}
};
const onFindEnter = (e) => {
if (e.key === 'Enter') {
e.preventDefault();
if(e.shiftKey) {
onFindPrev();
} else {
onFindNext();
}
}
};
const onReplaceEnter = (e) => {
if (e.key === 'Enter') {
e.preventDefault();
onReplace();
}
};
const onEscape = (e)=>{
if (e.key === 'Escape') {
e.preventDefault();
clearAndClose();
}
};
const onFindNext = ()=>{
if(searchCursor.current?.find()) {
editor.setSelection(searchCursor.current.from(), searchCursor.current.to());
editor.scrollIntoView({
from: searchCursor.current.from(),
to: searchCursor.current.to()
}, 20);
}
};
const onFindPrev = ()=>{
if(searchCursor.current?.find(true)) {
editor.setSelection(searchCursor.current.from(), searchCursor.current.to());
editor.scrollIntoView({
from: searchCursor.current.from(),
to: searchCursor.current.to()
}, 20);
}
};
const onReplace = ()=>{
searchCursor.current.replace(replaceVal);
onFindNext();
};
const onReplaceAll = ()=>{
/* search from start */
search();
while(searchCursor.current.from()) {
onReplace();
}
};
if(!editor) {
return <></>;
}
return (
<Box className={classes.findDialog} visibility={show ? 'visible' : 'hidden'} tabIndex="0" onKeyDown={onEscape}>
<InputText value={findVal}
inputRef={(ele)=>{findInputRef.current = ele;}}
onChange={(value)=>setFindVal(value)}
onKeyPress={onFindEnter}
placeholder={gettext('Find text')}
controlProps={{
title: gettext('Find text')
}}
endAdornment={
<InputAdornment position="end">
<PgIconButton data-test="case" title="Match case" icon={<FormatCaseIcon />} size="xs" noBorder
onClick={()=>toggle('case')} color={matchCase ? 'primary' : 'default'} style={{marginRight: '2px'}}/>
<PgIconButton data-test="regex" title="Use regex" icon={<RegexIcon />} size="xs" noBorder
onClick={()=>toggle('regex')} color={useRegex ? 'primary' : 'default'}/>
</InputAdornment>
}
/>
{replace &&
<InputText value={replaceVal}
className={classes.marginTop}
onChange={(value)=>setReplaceVal(value)}
onKeyPress={onReplaceEnter}
placeholder={gettext('Replace value')}
controlProps={{
title: gettext('Replace value')
}}
/>}
<Box display="flex" className={classes.marginTop}>
<PgIconButton title={gettext('Previous')} icon={<ArrowUpwardRoundedIcon />} size="xs" noBorder onClick={onFindPrev}
style={{marginRight: '2px'}} />
<PgIconButton title={gettext('Next')} icon={<ArrowDownwardRoundedIcon />} size="xs" noBorder onClick={onFindNext}
style={{marginRight: '2px'}} />
{replace && <>
<PgIconButton title={gettext('Replace')} icon={<SwapHorizRoundedIcon style={{height: 'unset'}}/>} size="xs" noBorder onClick={onReplace}
style={{marginRight: '2px'}} />
<PgIconButton title={gettext('Replace All')} icon={<SwapCallsRoundedIcon />} size="xs" noBorder onClick={onReplaceAll}/>
</>}
<Box marginLeft="auto">
<PgIconButton title={gettext('Close')} icon={<CloseIcon />} size="xs" noBorder onClick={clearAndClose}/>
</Box>
</Box>
</Box>
);
}
FindDialog.propTypes = {
editor: CodeMirrorInstancType,
show: PropTypes.bool,
replace: PropTypes.bool,
onClose: PropTypes.func,
selFindVal: PropTypes.string,
};
export function CopyButton({show, copyText}) {
const classes = useStyles();
const [copyBtnLabel, setCopyBtnLabel] = useState(gettext('Copy'));
const revertCopiedText = useDelayedCaller(()=>{
setCopyBtnLabel(gettext('Copy'));
});
return (
<Box className={classes.copyButton} visibility={show ? 'visible' : 'hidden'}>
<DefaultButton onClick={() => {
copyToClipboard(copyText);
setCopyBtnLabel(gettext('Copied!'));
revertCopiedText(1500);
}}>{copyBtnLabel}</DefaultButton>
</Box>
);
}
CopyButton.propTypes = {
show: PropTypes.bool,
copyText: PropTypes.string
};
function handleDrop(editor, e) {
let dropDetails = null;
try {
dropDetails = JSON.parse(e.dataTransfer.getData('text'));
/* Stop firefox from redirecting */
if(e.preventDefault) {
e.preventDefault();
}
if (e.stopPropagation) {
e.stopPropagation();
}
} catch(error) {
/* if parsing fails, it must be the drag internal of codemirror text */
return;
}
let cursor = editor.coordsChar({
left: e.x,
top: e.y,
});
editor.replaceRange(dropDetails.text, cursor);
editor.focus();
editor.setSelection({
...cursor,
ch: cursor.ch + dropDetails.cur.from,
},{
...cursor,
ch: cursor.ch +dropDetails.cur.to,
});
}
function calcFontSize(fontSize) {
if(fontSize) {
fontSize = parseFloat((Math.round(parseFloat(fontSize + 'e+2')) + 'e-2'));
let rounded = Number(fontSize);
if(rounded > 0) {
return rounded + 'em';
}
}
return '1em';
}
async function handlePaste(_editor, e) {
let copiedText = await e.clipboardData.getData('text');
checkTrojanSource(copiedText, true);
}
/* React wrapper for CodeMirror */
export default function CodeMirror({currEditor, name, value, options, events, readonly, disabled, className, autocomplete=false, gutters=['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], showCopyBtn=false, cid, helpid}) {
const taRef = useRef();
const editor = useRef();
const cmWrapper = useRef();
const isVisibleTrack = useRef();
const classes = useStyles();
const [[showFind, isReplace], setShowFind] = useState([false, false]);
const [showCopy, setShowCopy] = useState(false);
const preferencesStore = usePreferences();
const defaultOptions = useMemo(()=>{
let goLeftKey = 'Ctrl-Alt-Left',
goRightKey = 'Ctrl-Alt-Right',
commentKey = 'Ctrl-/';
if(isMac()) {
goLeftKey = 'Cmd-Alt-Left';
goRightKey = 'Cmd-Alt-Right';
commentKey = 'Cmd-/';
}
return {
tabindex: '0',
lineNumbers: true,
styleSelectedText: true,
mode: 'text/x-pgsql',
foldOptions: {
widget: '\u2026',
},
foldGutter: true,
gutters: gutters,
extraKeys: {
// Autocomplete sql command
...(autocomplete ? {
'Ctrl-Space': 'autocomplete',
}: {}),
'Alt-Up': 'goLineUp',
'Alt-Down': 'goLineDown',
// Move word by word left/right
[goLeftKey]: 'goGroupLeft',
[goRightKey]: 'goGroupRight',
// Allow user to delete Tab(s)
'Shift-Tab': 'indentLess',
//comment
[commentKey]: 'toggleComment',
},
dragDrop: true,
screenReaderLabel: gettext('SQL editor'),
};
});
useEffect(()=>{
const finalOptions = {...defaultOptions, ...options};
/* Create the object only once on mount */
editor.current = new OrigCodeMirror.fromTextArea(
taRef.current, finalOptions);
if(!_.isEmpty(value)) {
editor.current.setValue(value);
} else {
editor.current.setValue('');
}
currEditor?.(editor.current);
if(editor.current) {
try {
cmWrapper.current = editor.current.getWrapperElement();
} catch(e) {
cmWrapper.current = null;
}
let findKey = 'Ctrl-F', replaceKey = 'Shift-Ctrl-F';
if(isMac()) {
findKey = 'Cmd-F';
replaceKey = 'Cmd-Alt-F';
}
editor.current.addKeyMap({
[findKey]: ()=>{
setShowFind([false, false]);
setShowFind([true, false]);
},
[replaceKey]: ()=>{
if(!finalOptions.readOnly) {
setShowFind([false, false]);
setShowFind([true, true]);
}
},
'Cmd-G': false,
});
}
Object.keys(events||{}).forEach((eventName)=>{
editor.current.on(eventName, events[eventName]);
});
editor.current.on('drop', handleDrop);
editor.current.on('paste', handlePaste);
return ()=>{
editor.current?.toTextArea();
};
}, []);
const autocompKeyup = (cm, event)=>{
if (!cm.state.completionActive && (event.key == 'Backspace' || /^[ -~]{1}$/.test(event.key))) {
OrigCodeMirror.commands.autocomplete(cm, null, {completeSingle: false});
}
};
useEffect(()=>{
let pref = preferencesStore.getPreferencesForModule('sqleditor');
let wrapEle = editor.current?.getWrapperElement();
wrapEle && (wrapEle.style.fontSize = calcFontSize(pref.sql_font_size));
// Register keyup event if autocomplete is true
if (autocomplete && pref.autocomplete_on_key_press) {
editor.current.on('keyup', autocompKeyup);
}
if(pref.plain_editor_mode) {
editor.current?.setOption('mode', 'text/plain');
/* Although not required, setting explicitly as codemirror will remove code folding only on next edit */
editor.current?.setOption('foldGutter', false);
} else {
editor.current?.setOption('mode', 'text/x-pgsql');
editor.current?.setOption('foldGutter', pref.code_folding);
}
editor.current?.setOption('indentWithTabs', !pref.use_spaces);
editor.current?.setOption('indentUnit', pref.tab_size);
editor.current?.setOption('tabSize', pref.tab_size);
editor.current?.setOption('lineWrapping', pref.wrap_code);
editor.current?.setOption('autoCloseBrackets', pref.insert_pair_brackets);
editor.current?.setOption('matchBrackets', pref.brace_matching);
editor.current?.refresh();
}, [preferencesStore]);
useEffect(()=>{
if(editor.current) {
if(readonly || disabled) {
editor.current.setOption('readOnly', true);
editor.current.addKeyMap({'Tab': false});
editor.current.addKeyMap({'Shift-Tab': false});
cmWrapper.current.classList.add(classes.hideCursor);
} else {
editor.current.setOption('readOnly', false);
editor.current.removeKeyMap('Tab');
editor.current.removeKeyMap('Shift-Tab');
cmWrapper.current.classList.remove(classes.hideCursor);
}
}
}, [readonly, disabled]);
useMemo(() => {
if(editor.current) {
if(value != editor.current.getValue()) {
if(!_.isEmpty(value)) {
editor.current.setValue(value);
} else {
editor.current.setValue('');
}
}
}
}, [value]);
const onScreenVisible = useOnScreen(cmWrapper);
if(!isVisibleTrack.current && onScreenVisible) {
isVisibleTrack.current = true;
editor.current?.refresh();
} else if(!onScreenVisible) {
isVisibleTrack.current = false;
}
const closeFind = ()=>{
setShowFind([false, false]);
editor.current?.focus();
};
return (
<div className={clsx(className, classes.root)}
onMouseEnter={() => { showCopyBtn && value.length > 0 && setShowCopy(true);}}
onMouseLeave={() => {showCopyBtn && setShowCopy(false);}}
>
<FindDialog editor={editor.current} show={showFind} replace={isReplace} onClose={closeFind} selFindVal={editor.current?.getSelection() && editor.current.getSelection().length > 0 ? editor.current.getSelection() : ''}/>
<CopyButton editor={editor.current} show={showCopy} copyText={value}></CopyButton>
<textarea ref={taRef} name={name} title={gettext('SQL editor')}
id={cid} aria-describedby={helpid} value={value??''} onChange={()=>{/* dummy */}}
/>
</div>
);
}
CodeMirror.propTypes = {
currEditor: PropTypes.func,
name: PropTypes.string,
value: PropTypes.string,
options: PropTypes.object,
change: PropTypes.func,
events: PropTypes.object,
readonly: PropTypes.bool,
disabled: PropTypes.bool,
className: CustomPropTypes.className,
autocomplete: PropTypes.bool,
gutters: PropTypes.array,
showCopyBtn: PropTypes.bool,
cid: PropTypes.string,
helpid: PropTypes.string,
};

View File

@ -32,7 +32,7 @@ import { KeyboardDateTimePicker, KeyboardDatePicker, KeyboardTimePicker, MuiPick
import DateFnsUtils from '@date-io/date-fns';
import * as DateFns from 'date-fns';
import CodeMirror from './CodeMirror';
import CodeMirror from './ReactCodeMirror';
import gettext from 'sources/gettext';
import _ from 'lodash';
import { DefaultButton, PrimaryButton, PgIconButton } from './Buttons';
@ -127,7 +127,7 @@ FormIcon.propTypes = {
};
/* Wrapper on any form component to add label, error indicator and help message */
export function FormInput({ children, error, className, label, helpMessage, required, testcid, withContainer=true, labelGridBasis=3, controlGridBasis=9 }) {
export function FormInput({ children, error, className, label, helpMessage, required, testcid, lid, withContainer=true, labelGridBasis=3, controlGridBasis=9 }) {
const classes = useStyles();
const cid = testcid || _.uniqueId('c');
const helpid = `h${cid}`;
@ -135,7 +135,7 @@ export function FormInput({ children, error, className, label, helpMessage, requ
return (
<>
<Grid item lg={labelGridBasis} md={labelGridBasis} sm={12} xs={12}>
<InputLabel htmlFor={cid} className={clsx(classes.formLabel, error ? classes.formLabelError : null)} required={required}>
<InputLabel id={lid} htmlFor={lid ? undefined : cid} className={clsx(classes.formLabel, error ? classes.formLabelError : null)} required={required}>
{label}
<FormIcon type={MESSAGE_TYPE.ERROR} style={{ marginLeft: 'auto', visibility: error ? 'unset' : 'hidden' }} />
</InputLabel>
@ -152,7 +152,7 @@ export function FormInput({ children, error, className, label, helpMessage, requ
return (
<Grid container spacing={0} className={className} data-testid="form-input">
<Grid item lg={labelGridBasis} md={labelGridBasis} sm={12} xs={12}>
<InputLabel htmlFor={cid} className={clsx(classes.formLabel, error ? classes.formLabelError : null)} required={required}>
<InputLabel id={lid} htmlFor={lid ? undefined : cid} className={clsx(classes.formLabel, error ? classes.formLabelError : null)} required={required}>
{label}
<FormIcon type={MESSAGE_TYPE.ERROR} style={{ marginLeft: 'auto', visibility: error ? 'unset' : 'hidden' }} />
</InputLabel>
@ -174,6 +174,7 @@ FormInput.propTypes = {
helpMessage: PropTypes.string,
required: PropTypes.bool,
testcid: PropTypes.any,
lid: PropTypes.any,
withContainer: PropTypes.bool,
labelGridBasis: PropTypes.number,
controlGridBasis: PropTypes.number,
@ -191,16 +192,10 @@ export function InputSQL({ value, options, onChange, className, controlProps, in
}}
value={value || ''}
options={{
lineNumbers: true,
mode: 'text/x-pgsql',
...options,
}}
className={clsx(classes.sql, className)}
events={{
change: (cm) => {
onChange?.(cm.getValue());
},
}}
onChange={onChange}
{...controlProps}
{...props}
/>
@ -220,9 +215,10 @@ export function FormInputSQL({ hasError, required, label, className, helpMessage
if (noLabel) {
return <InputSQL value={value} options={controlProps} {...props} />;
} else {
const lid = _.uniqueId('l');
return (
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid} >
<InputSQL value={value} options={controlProps} {...props} />
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid} lid={lid}>
<InputSQL value={value} options={controlProps} labelledBy={lid} {...props} />
</FormInput>
);
}

View File

@ -0,0 +1,150 @@
import {
EditorView
} from '@codemirror/view';
import { StateEffect, EditorState } from '@codemirror/state';
import { autocompletion } from '@codemirror/autocomplete';
import {undo} from '@codemirror/commands';
import { errorMarkerEffect } from './extensions/errorMarker';
import { activeLineEffect, activeLineField } from './extensions/activeLineMarker';
import { clearBreakpoints, hasBreakpoint, toggleBreakpoint } from './extensions/breakpointGutter';
function getAutocompLoading({ bottom, left }, dom) {
const cmRect = dom.getBoundingClientRect();
const div = document.createElement('div');
div.classList.add('cm-tooltip', 'pg-autocomp-loader');
div.innerText = 'Loading...';
div.style.position = 'absolute';
div.style.top = (bottom - cmRect.top) + 'px';
div.style.left = (left - cmRect.left) + 'px';
dom?.appendChild(div);
return div;
}
export default class CustomEditorView extends EditorView {
constructor(...args) {
super(...args);
this._cleanDoc = this.state.doc;
}
getValue() {
return this.state.doc.toString();
}
setValue(newValue, markClean=false) {
newValue = newValue || '';
if(markClean) {
// create a new doc with new value to make it clean
this._cleanDoc = EditorState.create({
doc: newValue
}).doc;
}
this.dispatch({
changes: { from: 0, to: this.getValue().length, insert: newValue }
});
}
getSelection() {
return this.state.sliceDoc(this.state.selection.main.from, this.state.selection.main.to) ?? '';
}
replaceSelection(newValue) {
this.dispatch(this.state.replaceSelection(newValue));
}
getCursor() {
let offset = this.state.selection.main.head;
let line = this.state.doc.lineAt(offset);
return {line: line.number, ch: offset - line.from};
}
setCursor(lineNo, ch) {
const n = this.state.doc.line(lineNo).from + ch;
this.dispatch({ selection: { anchor: n, head: n } });
}
getCurrentLineNo() {
return this.state.doc.lineAt(this.state.selection.main.head).number;
}
lineCount() {
return this.state.doc.lines;
}
getLine(lineNo) {
// line is 1-based;
return this.state.doc.line(lineNo).text;
}
getActiveLine() {
const activeLineChunk = this.state.field(activeLineField).chunkPos;
if(activeLineChunk.length > 0) {
return this.state.doc.lineAt(activeLineChunk[0]).number;
}
return undefined;
}
hasBreakpoint(lineNo) {
const line = this.state.doc.line(lineNo);
return hasBreakpoint(this, line.from);
}
toggleBreakpoint(lineNo, silent, val) {
const line = this.state.doc.line(lineNo);
toggleBreakpoint(this, line.from, silent, val);
}
clearBreakpoints() {
clearBreakpoints(this);
}
markClean() {
this._cleanDoc = this.state.doc;
}
isDirty() {
return !this._cleanDoc.eq(this.state.doc);
}
undo() {
return undo(this);
}
fireDOMEvent(event) {
this.contentDOM.dispatchEvent(event);
}
registerAutocomplete(completionFunc) {
this.dispatch({
effects: StateEffect.appendConfig.of(
autocompletion({
override: [(context) => {
this.loadingDiv?.remove();
this.loadingDiv = getAutocompLoading(this.coordsAtPos(context.pos), this.dom);
context.addEventListener('abort', () => {
this.loadingDiv?.remove();
});
return Promise.resolve(completionFunc(context, () => {
this.loadingDiv?.remove();
}));
}]
}
))
});
}
setErrorMark(fromCursor, toCursor) {
const from = this.state.doc.line(fromCursor.line).from + fromCursor.pos;
const to = this.state.doc.line(toCursor.line).from + toCursor.pos;
this.dispatch({ effects: errorMarkerEffect.of({ from, to }) });
}
removeErrorMark() {
this.dispatch({ effects: errorMarkerEffect.of({ clear: true }) });
}
setActiveLine(line) {
this.dispatch({ effects: activeLineEffect.of({ from: line, to: line }) });
}
}

View File

@ -0,0 +1,432 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2023, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import ReactDOMServer from 'react-dom/server';
import PropTypes from 'prop-types';
import gettext from 'sources/gettext';
import { makeStyles } from '@material-ui/core';
import { PgIconButton } from '../Buttons';
import { checkTrojanSource } from '../../utils';
import { copyToClipboard } from '../../clipboard';
import { useDelayedCaller } from '../../custom_hooks';
import usePreferences from '../../../../preferences/static/js/store';
import FileCopyRoundedIcon from '@material-ui/icons/FileCopyRounded';
import CheckRoundedIcon from '@material-ui/icons/CheckRounded';
import KeyboardArrowRightRoundedIcon from '@material-ui/icons/KeyboardArrowRightRounded';
import ExpandMoreRoundedIcon from '@material-ui/icons/ExpandMoreRounded';
// Codemirror packages
import {
lineNumbers,
highlightSpecialChars,
drawSelection,
dropCursor,
highlightActiveLine,
EditorView,
keymap,
} from '@codemirror/view';
import { EditorState, Compartment } from '@codemirror/state';
import { history, defaultKeymap, historyKeymap, indentLess, insertTab } from '@codemirror/commands';
import { highlightSelectionMatches } from '@codemirror/search';
import { closeBrackets, autocompletion, closeBracketsKeymap, completionKeymap } from '@codemirror/autocomplete';
import {
foldGutter,
indentOnInput,
bracketMatching,
indentUnit,
foldKeymap,
} from '@codemirror/language';
import FindDialog from './FindDialog';
import syntaxHighlighting from './extensions/highlighting';
import PgSQL from './extensions/dialect';
import { sql } from '@codemirror/lang-sql';
import errorMarkerExtn from './extensions/errorMarker';
import CustomEditorView from './CustomEditorView';
import breakpointGutter, { breakpointEffect } from './extensions/breakpointGutter';
import activeLineExtn from './extensions/activeLineMarker';
const arrowRightHtml = ReactDOMServer.renderToString(<KeyboardArrowRightRoundedIcon style={{fontSize: '1.2em'}} />);
const arrowDownHtml = ReactDOMServer.renderToString(<ExpandMoreRoundedIcon style={{fontSize: '1.2em'}} />);
const useStyles = makeStyles(() => ({
copyButton: {
position: 'absolute',
zIndex: 99,
right: '4px',
top: '4px',
}
}));
export function CopyButton({ editor }) {
const classes = useStyles();
const [isCopied, setIsCopied] = useState(false);
const revertCopiedText = useDelayedCaller(() => {
setIsCopied(false);
});
return (
<PgIconButton size="small" className={classes.copyButton} icon={isCopied ? <CheckRoundedIcon /> : <FileCopyRoundedIcon />}
title={isCopied ? gettext('Copied!') : gettext('Copy')}
onClick={() => {
copyToClipboard(editor?.getValue());
setIsCopied(true);
revertCopiedText(1500);
}}
/>
);
}
CopyButton.propTypes = {
editor: PropTypes.object,
};
function handleDrop(e, editor) {
let dropDetails = null;
try {
dropDetails = JSON.parse(e.dataTransfer.getData('text'));
/* Stop firefox from redirecting */
if (e.preventDefault) {
e.preventDefault();
}
if (e.stopPropagation) {
e.stopPropagation();
}
} catch (error) {
/* if parsing fails, it must be the drag internal of codemirror text */
// editor.inputState.handlers.drop(e, editor);
return false;
}
const dropPos = editor.posAtCoords({ x: e.x, y: e.y });
editor.dispatch({
changes: { from: dropPos, to: dropPos, insert: dropDetails.text || '' },
selection: { anchor: dropPos + dropDetails.cur.from, head: dropPos + dropDetails.cur.to }
});
editor.focus();
}
function calcFontSize(fontSize) {
if (fontSize) {
fontSize = parseFloat((Math.round(parseFloat(fontSize + 'e+2')) + 'e-2'));
let rounded = Number(fontSize);
if (rounded > 0) {
return rounded + 'em';
}
}
return '1em';
}
function handlePaste(e) {
let copiedText = e.clipboardData.getData('text');
checkTrojanSource(copiedText, true);
}
/* React wrapper for CodeMirror */
const defaultExtensions = [
highlightSpecialChars(),
drawSelection(),
dropCursor(),
EditorState.allowMultipleSelections.of(true),
indentOnInput(),
syntaxHighlighting,
highlightSelectionMatches(),
keymap.of([defaultKeymap, closeBracketsKeymap, historyKeymap, foldKeymap, completionKeymap].flat()),
keymap.of([{
key: 'Tab',
preventDefault: true,
run: insertTab,
},
{
key: 'Shift-Tab',
preventDefault: true,
run: indentLess,
}]),
sql({
dialect: PgSQL,
}),
PgSQL.language.data.of({
autocomplete: false,
}),
EditorView.domEventHandlers({
drop: handleDrop,
paste: handlePaste,
}),
errorMarkerExtn()
];
export default function Editor({
currEditor, name, value, options, onCursorActivity, onChange, readonly, disabled, autocomplete = false,
breakpoint = false, onBreakPointChange, showActiveLine=false, showCopyBtn = false,
keepHistory = true, cid, helpid, labelledBy}) {
const [[showFind, isReplace], setShowFind] = useState([false, false]);
const [showCopy, setShowCopy] = useState(false);
const editorContainerRef = useRef();
const editor = useRef();
const defaultOptions = {
lineNumbers: true,
foldGutter: true,
};
const preferencesStore = usePreferences();
const editable = !disabled;
const configurables = useRef(new Compartment());
const editableConfig = useRef(new Compartment());
const findDialogKeyMap = [{
key: 'Mod-f', run: (view, e) => {
e.preventDefault();
e.stopPropagation();
setShowFind([false, false]);
setShowFind([true, false]);
}
}, {
key: 'Mod-Alt-f', run: (view, e) => {
e.preventDefault();
e.stopPropagation();
setShowFind([false, false]);
setShowFind([true, true]);
},
}];
useEffect(() => {
const finalOptions = { ...defaultOptions, ...options };
const finalExtns = [
...defaultExtensions,
];
if (finalOptions.lineNumbers) {
finalExtns.push(lineNumbers());
}
if (finalOptions.foldGutter) {
finalExtns.push(foldGutter({
markerDOM: (open)=>{
let icon = document.createElement('span');
if(open) {
icon.innerHTML = arrowDownHtml;
} else {
icon.innerHTML = arrowRightHtml;
}
return icon;
}
}));
}
if (editorContainerRef.current) {
const state = EditorState.create({
extensions: [
...finalExtns,
configurables.current.of([]),
keymap.of(findDialogKeyMap),
editableConfig.current.of([
EditorView.editable.of(!disabled),
EditorState.readOnly.of(readonly),
].concat(keepHistory ? [history()] : [])),
[EditorView.updateListener.of(function(update) {
if(update.selectionSet) {
onCursorActivity?.(update.view.getCursor(), update.view);
}
if(update.docChanged) {
onChange?.(update.view.getValue(), update.view);
}
if(breakpoint) {
for(const transaction of update.transactions) {
for(const effect of transaction.effects) {
if(effect.is(breakpointEffect)) {
if(effect.value.silent) {
/* do nothing */
return;
}
const lineNo = editor.current.state.doc.lineAt(effect.value.pos).number;
onBreakPointChange?.(lineNo, effect.value.on);
}
}
}
}
})],
EditorView.contentAttributes.of({
id: cid,
'aria-describedby': helpid,
'aria-labelledby': labelledBy,
}),
breakpoint ? breakpointGutter : [],
showActiveLine ? highlightActiveLine() : activeLineExtn(),
],
});
editor.current = new CustomEditorView({
state,
parent: editorContainerRef.current
});
if(!_.isEmpty(value)) {
editor.current.setValue(value);
} else {
editor.current.setValue('');
}
currEditor?.(editor.current);
}
return () => {
editor.current?.destroy();
};
}, []);
useMemo(() => {
if(editor.current) {
if(value != editor.current.getValue()) {
if(!_.isEmpty(value)) {
editor.current.setValue(value);
} else {
editor.current.setValue('');
}
}
}
}, [value]);
useEffect(() => {
let pref = preferencesStore.getPreferencesForModule('sqleditor');
let newConfigExtn = [];
const fontSize = calcFontSize(pref.sql_font_size);
newConfigExtn.push(EditorView.theme({
'.cm-content': {
fontSize: fontSize,
},
'.cm-gutters': {
fontSize: fontSize,
},
}));
const autoCompOptions = {
icons: false,
addToOptions: [{
render: (completion) => {
const element = document.createElement('div');
if (completion.type == 'keyword') {
element.className = 'cm-completionIcon cm-completionIcon-keyword';
} else if (completion.type == 'property') {
// CM adds columns as property, although we have changed this.
element.className = 'pg-cm-autocomplete-icon icon-column';
} else if (completion.type == 'type') {
// CM adds table as type
element.className = 'pg-cm-autocomplete-icon icon-table';
} else {
element.className = 'pg-cm-autocomplete-icon icon-' + completion.type;
}
return element;
},
position: 20,
}],
};
if (autocomplete) {
if (pref.autocomplete_on_key_press) {
newConfigExtn.push(autocompletion({
...autoCompOptions,
activateOnTyping: true,
}));
} else {
newConfigExtn.push(autocompletion({
...autoCompOptions,
activateOnTyping: false,
}));
}
}
newConfigExtn.push(
EditorState.tabSize.of(pref.tab_size),
);
if (pref.use_spaces) {
newConfigExtn.push(
indentUnit.of(new Array(pref.tab_size).fill(' ').join('')),
);
} else {
newConfigExtn.push(
indentUnit.of('\t'),
);
}
if (pref.wrap_code) {
newConfigExtn.push(
EditorView.lineWrapping
);
}
if (pref.insert_pair_brackets) {
newConfigExtn.push(closeBrackets());
}
if (pref.brace_matching) {
newConfigExtn.push(bracketMatching());
}
editor.current.dispatch({
effects: configurables.current.reconfigure(newConfigExtn)
});
}, [preferencesStore]);
useMemo(() => {
if (editor.current) {
if (value != editor.current.getValue()) {
editor.current.dispatch({
changes: { from: 0, to: editor.current.state.doc.length, insert: value || '' }
});
}
}
}, [value]);
useEffect(() => {
editor.current?.dispatch({
effects: editableConfig.current.reconfigure([
EditorView.editable.of(editable),
EditorState.readOnly.of(readonly),
].concat(keepHistory ? [history()] : []))
});
}, [readonly, disabled, keepHistory]);
const closeFind = () => {
setShowFind([false, false]);
editor.current?.focus();
};
const onMouseEnter = useCallback(()=>{showCopyBtn && setShowCopy(true);});
const onMouseLeave = useCallback(()=>{showCopyBtn && setShowCopy(false);});
return (
<div onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} style={{height: '100%'}}>
<div style={{ height: '100%' }} ref={editorContainerRef} name={name}></div>
{showCopy && <CopyButton editor={editor.current} />}
{showFind &&
<FindDialog editor={editor.current} show={showFind} replace={isReplace} onClose={closeFind} />
}
</div>
);
}
Editor.propTypes = {
currEditor: PropTypes.func,
name: PropTypes.string,
value: PropTypes.string,
options: PropTypes.object,
onCursorActivity: PropTypes.func,
onChange: PropTypes.func,
readonly: PropTypes.bool,
disabled: PropTypes.bool,
autocomplete: PropTypes.bool,
breakpoint: PropTypes.bool,
onBreakPointChange: PropTypes.func,
showActiveLine: PropTypes.bool,
showCopyBtn: PropTypes.bool,
keepHistory: PropTypes.bool,
cid: PropTypes.string,
helpid: PropTypes.string,
labelledBy: PropTypes.string,
};

View File

@ -0,0 +1,203 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2023, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React, { useEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import gettext from 'sources/gettext';
import { Box, InputAdornment, makeStyles } from '@material-ui/core';
import { InputText } from '../FormComponents';
import { PgIconButton } from '../Buttons';
import CloseIcon from '@material-ui/icons/CloseRounded';
import ArrowDownwardRoundedIcon from '@material-ui/icons/ArrowDownwardRounded';
import ArrowUpwardRoundedIcon from '@material-ui/icons/ArrowUpwardRounded';
import SwapHorizRoundedIcon from '@material-ui/icons/SwapHorizRounded';
import SwapCallsRoundedIcon from '@material-ui/icons/SwapCallsRounded';
import { RegexIcon, FormatCaseIcon } from '../ExternalIcon';
import {
openSearchPanel,
closeSearchPanel,
setSearchQuery,
SearchQuery,
findNext,
findPrevious,
replaceNext,
replaceAll,
} from '@codemirror/search';
const useStyles = makeStyles((theme)=>({
root: {
position: 'absolute',
zIndex: 99,
right: '4px',
top: '0px',
...theme.mixins.panelBorder.all,
borderTop: 'none',
padding: '2px 4px',
width: '250px',
backgroundColor: theme.palette.background.default,
},
marginTop: {
marginTop: '0.25rem',
},
}));
export default function FindDialog({editor, show, replace, onClose}) {
const [findVal, setFindVal] = useState(editor?.getSelection());
const [replaceVal, setReplaceVal] = useState('');
const [useRegex, setUseRegex] = useState(false);
const [matchCase, setMatchCase] = useState(false);
const findInputRef = useRef();
const searchQuery = useRef();
const classes = useStyles();
const search = ()=>{
if(editor) {
let query = new SearchQuery({
search: findVal,
caseSensitive: matchCase,
regexp: useRegex,
wholeWord: false,
replace: replaceVal,
});
if ((searchQuery.current && !query.eq(searchQuery.current))
|| !searchQuery.current) {
searchQuery.current = query;
editor.dispatch({effects: setSearchQuery.of(query)});
}
}
};
useEffect(()=>{
if(show) {
openSearchPanel(editor);
// Get selected text from editor and set it to find/replace input.
let selText = editor.getSelection();
setFindVal(selText);
findInputRef.current && findInputRef.current.select();
}
}, [show]);
useEffect(()=>{
search();
}, [findVal, replaceVal, useRegex, matchCase]);
const clearAndClose = ()=>{
onClose();
closeSearchPanel(editor);
};
const toggle = (name)=>{
if(name == 'regex') {
setUseRegex((prev)=>!prev);
} else if(name == 'case') {
setMatchCase((prev)=>!prev);
}
};
const onFindEnter = (e) => {
if (e.key === 'Enter') {
e.preventDefault();
if(e.shiftKey) {
onFindPrev();
} else {
onFindNext();
}
}
};
const onReplaceEnter = (e) => {
if (e.key === 'Enter') {
e.preventDefault();
onReplace();
}
};
const onEscape = (e)=>{
if (e.key === 'Escape') {
e.preventDefault();
clearAndClose();
}
};
const onFindNext = ()=>{
findNext(editor);
};
const onFindPrev = ()=>{
findPrevious(editor);
};
const onReplace = ()=>{
replaceNext(editor);
};
const onReplaceAll = ()=>{
replaceAll(editor);
};
if(!editor) {
return <></>;
}
return (
<Box className={classes.root} visibility={show ? 'visible' : 'hidden'} tabIndex="0" onKeyDown={onEscape}>
<InputText value={findVal}
inputRef={(ele)=>{findInputRef.current = ele;}}
onChange={(value)=>setFindVal(value)}
onKeyPress={onFindEnter}
endAdornment={
<InputAdornment position="end">
<PgIconButton data-test="case" title="Match case" icon={<FormatCaseIcon />} size="xs" noBorder
onClick={()=>toggle('case')} color={matchCase ? 'primary' : 'default'} style={{marginRight: '2px'}}/>
<PgIconButton data-test="regex" title="Use regex" icon={<RegexIcon />} size="xs" noBorder
onClick={()=>toggle('regex')} color={useRegex ? 'primary' : 'default'}/>
</InputAdornment>
}
/>
{replace &&
<InputText value={replaceVal}
className={classes.marginTop}
onChange={(value)=>setReplaceVal(value)}
onKeyPress={onReplaceEnter}
/>}
<Box display="flex" className={classes.marginTop}>
<PgIconButton title={gettext('Previous')} icon={<ArrowUpwardRoundedIcon />} size="xs" noBorder onClick={onFindPrev}
style={{marginRight: '2px'}} />
<PgIconButton title={gettext('Next')} icon={<ArrowDownwardRoundedIcon />} size="xs" noBorder onClick={onFindNext}
style={{marginRight: '2px'}} />
{replace && <>
<PgIconButton title={gettext('Replace')} icon={<SwapHorizRoundedIcon style={{height: 'unset'}}/>} size="xs" noBorder onClick={onReplace}
style={{marginRight: '2px'}} />
<PgIconButton title={gettext('Replace All')} icon={<SwapCallsRoundedIcon />} size="xs" noBorder onClick={onReplaceAll}/>
</>}
<Box marginLeft="auto">
<PgIconButton title={gettext('Close')} icon={<CloseIcon />} size="xs" noBorder onClick={clearAndClose}/>
</Box>
</Box>
</Box>
);
}
export const CodeMirrorInstanceType = PropTypes.shape({
getValue: PropTypes.func,
setValue: PropTypes.func,
getSelection: PropTypes.func,
dispatch: PropTypes.func,
});
FindDialog.propTypes = {
editor: CodeMirrorInstanceType,
show: PropTypes.bool,
replace: PropTypes.bool,
onClose: PropTypes.func,
selFindVal: PropTypes.string,
};

View File

@ -0,0 +1,33 @@
import {
EditorView,
Decoration,
} from '@codemirror/view';
import { StateEffect, StateField } from '@codemirror/state';
export const activeLineEffect = StateEffect.define({
map: ({ from, to }, change) => ({ from: change.mapPos(from), to: change.mapPos(to) })
});
const activeLineDeco = Decoration.line({ class: 'cm-activeLine' });
export const activeLineField = StateField.define({
create() {
return Decoration.none;
},
update(value, tr) {
for (let e of tr.effects) if (e.is(activeLineEffect)) {
if(e.value.clear || e.value.from == -1) {
return Decoration.none;
}
const line = tr.state.doc.line(e.value.from);
return Decoration.set([activeLineDeco.range(line.from)]);
}
return value;
},
provide: f => EditorView.decorations.from(f)
});
export default function activeLineExtn() {
return [activeLineField];
}

View File

@ -0,0 +1,67 @@
import {GutterMarker, gutter} from '@codemirror/view';
import {StateField, StateEffect, RangeSet} from '@codemirror/state';
export const breakpointEffect = StateEffect.define({
map: (val, mapping) => {
return {pos: mapping.mapPos(val.pos), on: val.on, clear: val.clear, silent: val.silent};
}
});
export const breakpointField = StateField.define({
create() { return RangeSet.empty; },
update(set, transaction) {
set = set.map(transaction.changes);
for (let e of transaction.effects) {
if (e.is(breakpointEffect)) {
if(e.value.clear) {
return RangeSet.empty;
}
if (e.value.on)
set = set.update({add: [breakpointMarker.range(e.value.pos)]});
else
set = set.update({filter: from => from != e.value.pos});
}
}
return set;
}
});
export function hasBreakpoint(view, pos) {
let breakpoints = view.state.field(breakpointField);
let has = false;
breakpoints.between(pos, pos, () => {has = true;});
return has;
}
export function toggleBreakpoint(view, pos, silent, val) {
view.dispatch({
effects: breakpointEffect.of({pos, on: typeof(val) == 'undefined' ? !hasBreakpoint(view, pos) : val, silent})
});
}
export function clearBreakpoints(view) {
view.dispatch({
effects: breakpointEffect.of({clear: true, silent: true})
});
}
const breakpointMarker = new class extends GutterMarker {
toDOM() { return document.createTextNode('●'); }
};
const breakpointGutter = [
breakpointField,
gutter({
class: 'cm-breakpoint-gutter',
markers: v => v.state.field(breakpointField),
initialSpacer: () => breakpointMarker,
domEventHandlers: {
mousedown(view, line) {
toggleBreakpoint(view, line.from);
return true;
}
}
}),
];
export default breakpointGutter;

View File

@ -0,0 +1,14 @@
import { SQLDialect, PostgreSQL } from '@codemirror/lang-sql';
const extraKeywords = 'unsafe';
const keywords = PostgreSQL.spec.keywords.replace(/\b\w\b/, '') + ' ' + extraKeywords;
const PgSQL = SQLDialect.define({
charSetCasts: true,
doubleDollarQuotedStrings: true,
operatorChars: '+-*/<>=~!@#%^&|`?',
specialVar: '',
keywords: keywords,
types: PostgreSQL.spec.types,
});
export default PgSQL;

View File

@ -0,0 +1,35 @@
import {
EditorView,
Decoration,
} from '@codemirror/view';
import { StateEffect, StateField } from '@codemirror/state';
export const errorMarkerEffect = StateEffect.define({
map: ({ from, to }, change) => ({ from: change.mapPos(from), to: change.mapPos(to) })
});
const errorMakerDeco = Decoration.mark({ class: 'cm-error-highlight' });
export const errorMakerField = StateField.define({
create() {
return Decoration.none;
},
update(underlines, tr) {
underlines = underlines.map(tr.changes);
for (let e of tr.effects) if (e.is(errorMarkerEffect)) {
if (e.value.clear) {
return Decoration.none;
}
underlines = underlines.update({
add: [errorMakerDeco.range(e.value.from, e.value.to)]
});
}
return underlines;
},
provide: f => EditorView.decorations.from(f)
});
export default function errorMarkerExtn() {
return [errorMakerField];
}

View File

@ -0,0 +1,41 @@
import {
syntaxHighlighting,
} from '@codemirror/language';
import {tagHighlighter, tags, classHighlighter} from '@lezer/highlight';
export const extendedClassHighlighter = tagHighlighter([
{tag: tags.link, class: 'tok-link'},
{tag: tags.heading, class: 'tok-heading'},
{tag: tags.emphasis, class: 'tok-emphasis'},
{tag: tags.strong, class: 'tok-strong'},
{tag: tags.keyword, class: 'tok-keyword'},
{tag: tags.atom, class: 'tok-atom'},
{tag: tags.bool, class: 'tok-bool'},
{tag: tags.url, class: 'tok-url'},
{tag: tags.labelName, class: 'tok-labelName'},
{tag: tags.inserted, class: 'tok-inserted'},
{tag: tags.deleted, class: 'tok-deleted'},
{tag: tags.literal, class: 'tok-literal'},
{tag: tags.string, class: 'tok-string'},
{tag: tags.number, class: 'tok-number'},
{tag: [tags.regexp, tags.escape, tags.special(tags.string)], class: 'tok-string2'},
{tag: tags.variableName, class: 'tok-variableName'},
{tag: tags.local(tags.variableName), class: 'tok-variableName tok-local'},
{tag: tags.definition(tags.variableName), class: 'tok-variableName tok-definition'},
{tag: tags.special(tags.variableName), class: 'tok-variableName2'},
{tag: tags.definition(tags.propertyName), class: 'tok-propertyName tok-definition'},
{tag: tags.typeName, class: 'tok-typeName'},
{tag: tags.namespace, class: 'tok-namespace'},
{tag: tags.className, class: 'tok-className'},
{tag: tags.macroName, class: 'tok-macroName'},
{tag: tags.propertyName, class: 'tok-propertyName'},
{tag: tags.operator, class: 'tok-operator'},
{tag: tags.comment, class: 'tok-comment'},
{tag: tags.meta, class: 'tok-meta'},
{tag: tags.invalid, class: 'tok-invalid'},
{tag: tags.punctuation, class: 'tok-punctuation'},
{tag: [tags.name, tags.deleted, tags.character, tags.propertyName, tags.macroName], class: 'tok-name'},
]);
export default syntaxHighlighting(classHighlighter);

View File

@ -0,0 +1,33 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2023, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React from 'react';
import { makeStyles } from '@material-ui/core';
import clsx from 'clsx';
import Editor from './Editor';
import CustomPropTypes from '../../custom_prop_types';
const useStyles = makeStyles(() => ({
root: {
position: 'relative',
},
}));
export default function CodeMirror({className, ...props}) {
const classes = useStyles();
return (
<div className={clsx(className, classes.root)}>
<Editor {...props} />
</div>
);
}
CodeMirror.propTypes = {
className: CustomPropTypes.className,
};

View File

@ -1,208 +0,0 @@
/* To override inbuilt Green color for matchingbracket */
.cm-s-default .CodeMirror-matchingbracket {
color: $sql-bracket-match-fg !important;
background-color: $sql-bracket-match-bg !important;
}
.CodeMirror {
font-size: 1em;
font-family: monospace, monospace;
background-color: $color-editor-bg !important;
color: $color-editor-fg;
border-radius: inherit;
}
/* Ensure the codemirror editor displays full height gutters when resized */
.CodeMirror, .CodeMirror-gutter {
height: 100% !important;
min-height: 100% !important;
}
/* class to disable Codemirror editor */
.cm_disabled {
background: $input-disabled-bg;
}
/* make syntax-highlighting bold */
.cm-s-default {
& .cm-quote {color: #090;}
& .cm-keyword {color: $color-editor-keyword; font-weight: 600;}
& .cm-atom {color: $color-editor-fg;}
& .cm-number {color: $color-editor-number; font-weight: 600;}
& .cm-def {color: $color-editor-fg;}
& .cm-punctuation,
& .cm-property,
& .cm-operator { color: $color-editor-operator; }
& .cm-variable {color: $color-editor-variable; }
& .cm-variable-2,
& .cm-variable-3,
& .cm-type {color: $color-editor-variable-2;}
& .cm-comment {color: $color-editor-comment;}
& .cm-string {color: $color-editor-string;}
& .cm-string-2 {color: $color-editor-string;}
& .cm-meta {color: $color-editor-fg;}
& .cm-qualifier {color: $color-editor-fg;}
& .cm-builtin {color: $color-editor-builtin;}
& .cm-bracket {color: $color-editor-bracket;}
& .cm-tag {color: $color-editor-fg;}
& .cm-attribute {color: $color-editor-fg;}
& .cm-hr {color: $color-editor-fg;}
& .cm-link {color: $color-editor-fg;}
& :not(.cm-fat-cursor) .CodeMirror-cursor {
border-left: thin solid $color-editor-fg;
border-right: none;
width: 0;
}
}
/* Codemirror buttons */
.CodeMirror-dialog button {
font-family: $font-family-primary;
color: $color-primary-fg;
font-size: 70%;
background-image: -webkit-linear-gradient(top, $color-primary-light 0%, $color-primary 100%);
background-image: -o-linear-gradient(top, $color-primary-light 0%, $color-primary 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from($color-primary-light), to($color-primary));
background-image: linear-gradient(to bottom, $color-primary-light 0%, $color-primary 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#{$color-primary-light}', endColorstr='#{$color-primary}', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: $color-primary;
border-radius: 4px;
}
.CodeMirror-gutters {
z-index: 2;
background-color: $sql-gutters-bg;
border-right: none;
}
/* workaround for codemirrors 'readOnly' option which is set to true instead of 'noCursor' */
.hide-cursor-workaround .CodeMirror-cursors {
display: none;
}
.CodeMirror-linenumber {
color: $color-fg;
font-size: 0.85em;
padding: 0;
}
.debugger-container .breakpoints {
width: 0.9em;
}
.CodeMirror, .CodeMirror-gutters {
min-height: 100%;
}
.CodeMirror-foldgutter {
width: .9em;
}
.breakpoints {
width: .9em;
cursor: pointer;
}
.CodeMirror-foldgutter-open,
.CodeMirror-foldgutter-folded {
cursor: pointer;
}
.CodeMirror-foldgutter-open:after {
content: "\25BC";
font-weight: 900;
}
.CodeMirror-foldgutter-folded:after {
content: "\25B6";
font-weight: 900;
}
.CodeMirror-foldmarker {
color: $color-editor-foldmarker;
text-shadow: #b9f 1px 1px 2px, #b9f -1px -1px 2px, #b9f 1px -1px 2px, #b9f -1px 1px 2px;
font-family: $font-family-primary;
line-height: .3;
cursor: pointer;
}
.CodeMirror-hints {
position: absolute;
z-index: 10;
overflow: hidden;
list-style: none;
margin: 0;
padding: 0px;
-webkit-box-shadow: $dropdown-box-shadow;
-moz-box-shadow: $dropdown-box-shadow;
box-shadow: $dropdown-box-shadow;
border-radius: $border-radius;
border: $panel-border;
background: $sql-hint-bg;
font-size: 90%;
font-family: $font-family-editor !important;
max-height: 20em;
overflow-y: auto;
& li {
padding: 0.125rem;
border-radius: 0rem;
&.CodeMirror-hint-active {
background: $sql-hint-active-bg;
color: $sql-hint-active-fg;
}
& .sqleditor-hint {
padding-left: 20px;
}
}
}
.CodeMirror .CodeMirror-selected {
background: $sql-editor-selection-bg !important;
}
.CodeMirror-activeline-background {
background: $color-editor-activeline !important;
}
.CodeMirror-simplescroll-horizontal {
height: $scrollbar-width;
}
.CodeMirror-simplescroll-vertical {
width: $scrollbar-width;
}
.CodeMirror-scrollbar-filler, .CodeMirror-simplescroll-horizontal, .CodeMirror-simplescroll-vertical {
background-color: lighten($scrollbar-base-color, 15%);
}
.CodeMirror-simplescroll-horizontal, .CodeMirror-simplescroll-vertical {
& div {
border: 0.25rem solid transparent;
border-radius: $border-radius*2;
background-clip: content-box;
background-color: rgba($scrollbar-base-color, 0.7);
&:hover {
background-color: $scrollbar-base-color;
}
}
}
.bg-gray-lighter {
background-color: $sql-editor-disable-bg !important;
}
.sql-editor-mark {
border-bottom: 2px dotted red;
}

View File

@ -838,10 +838,6 @@ table.table-empty-rows{
.filter-textarea {
height: 100%;
& .CodeMirror-scroll {
min-height: 120px;
max-height: 120px;
}
}
.dataview_filter_dialog {

View File

@ -17,7 +17,6 @@ $theme-colors: (
--psql-selection: #{$psql-selection};
}
@import 'codemirror.overrides';
@import 'pgadmin.style';
@import 'jsoneditor.overrides';
@import 'rc-dock/dist/rc-dock.css';

View File

@ -114,13 +114,13 @@ export default function DebuggerComponent({ pgAdmin, selectedNodeInfo, panelId,
url: baseUrl,
method: 'POST',
data: {
'breakpoint_list': breakpoint_list.lenght > 0 ? breakpoint_list.join() : null,
'breakpoint_list': breakpoint_list.length > 0 ? breakpoint_list.join() : null,
},
})
.then(function (res) {
if (res.data.data.status) {
executeQuery(transId);
setUnsetBreakpoint(res, breakpoint_list);
editor.current.clearBreakpoints();
}
enableToolbarButtons();
})
@ -158,7 +158,8 @@ export default function DebuggerComponent({ pgAdmin, selectedNodeInfo, panelId,
}
};
const raisePollingError = () => {
const raisePollingError = (error) => {
console.error(error);
pgAdmin.Browser.notifier.alert(
gettext('Debugger Error'),
gettext('Error while polling result.')
@ -263,7 +264,7 @@ export default function DebuggerComponent({ pgAdmin, selectedNodeInfo, panelId,
) {
editor.current.setValue(res.data.data.result[0].src);
setActiveLine(res.data.data.result[0].linenumber - 2);
editor.current.setActiveLine(res.data.data.result[0].linenumber - 1);
}
// Call function to create and update Stack information ....
getStackInformation(transId);
@ -277,7 +278,8 @@ export default function DebuggerComponent({ pgAdmin, selectedNodeInfo, panelId,
);
}
})
.catch(function () {
.catch(function (error) {
console.error(error);
pgAdmin.Browser.notifier.alert(
gettext('Debugger Error'),
gettext('Error while executing requested debugging information.')
@ -285,31 +287,6 @@ export default function DebuggerComponent({ pgAdmin, selectedNodeInfo, panelId,
});
};
const setActiveLine = (lineNo) => {
/* If lineNo sent, remove active line */
if (lineNo && editor.current.activeLineNo) {
editor.current.removeLineClass(
editor.current.activeLineNo, 'wrap', 'CodeMirror-activeline-background'
);
}
/* If lineNo not sent, set it to active line */
if (!lineNo && editor.current.activeLineNo) {
lineNo = editor.current.activeLineNo;
}
/* Set new active line only if positive */
if (lineNo > 0) {
editor.current.activeLineNo = lineNo;
editor.current.addLineClass(
editor.current.activeLineNo, 'wrap', 'CodeMirror-activeline-background'
);
/* centerOnLine is codemirror extension in bundle/codemirror.js */
editor.current.centerOnLine(editor.current.activeLineNo);
}
};
const selectFrame = (frameId) => {
// Make ajax call to listen the database message
let baseUrl = url_for('debugger.select_frame', {
@ -324,7 +301,7 @@ export default function DebuggerComponent({ pgAdmin, selectedNodeInfo, panelId,
if (res.data.data.status) {
editor.current.setValue(res.data.data.result[0].src);
updateBreakpoint(params.transId, true);
setActiveLine(res.data.data.result[0].linenumber - 2);
editor.current.setActiveLine(res.data.data.result[0].linenumber - 1);
}
})
.catch(function () {
@ -386,20 +363,6 @@ export default function DebuggerComponent({ pgAdmin, selectedNodeInfo, panelId,
};
}, []);
const setUnsetBreakpoint = (res, breakpoint_list) => {
if (res.data.data.status) {
for (let brk_val of breakpoint_list) {
let info = editor.current.lineInfo((brk_val - 1));
if (info) {
if (info.gutterMarkers != undefined) {
editor.current.setGutterMarker((brk_val - 1), 'breakpoints', null);
}
}
}
}
};
const triggerClearBreakpoint = () => {
let clearBreakpoint = (br_list) => {
// If there is no break point to clear then we should return from here.
@ -421,8 +384,8 @@ export default function DebuggerComponent({ pgAdmin, selectedNodeInfo, panelId,
'breakpoint_list': breakpoint_list.join(),
},
})
.then(function (res) {
setUnsetBreakpoint(res, breakpoint_list);
.then(function () {
editor.current.clearBreakpoints();
enableToolbarButtons();
})
.catch(raiseClearBrekpointError);
@ -448,63 +411,11 @@ export default function DebuggerComponent({ pgAdmin, selectedNodeInfo, panelId,
};
const debuggerMark = () => {
let marker = document.createElement('div');
marker.style.color = '#822';
marker.innerHTML = '●';
return marker;
};
const triggerToggleBreakpoint = () => {
disableToolbarButtons();
let info = editor.current.lineInfo(editor.current.activeLineNo);
let baseUrl = '';
// If gutterMarker is undefined that means there is no marker defined previously
// So we need to set the breakpoint command here...
if (info.gutterMarkers == undefined) {
baseUrl = url_for('debugger.set_breakpoint', {
'trans_id': params.transId,
'line_no': editor.current.activeLineNo + 1,
'set_type': '1',
});
} else {
baseUrl = url_for('debugger.set_breakpoint', {
'trans_id': params.transId,
'line_no': editor.current.activeLineNo + 1,
'set_type': '0',
});
}
api({
url: baseUrl,
method: 'GET',
})
.then(function (res) {
if (res.data.data.status) {
// Call function to create and update local variables ....
let info_local = editor.current.lineInfo(editor.current.activeLineNo);
if (info_local.gutterMarkers != undefined) {
editor.current.setGutterMarker(editor.current.activeLineNo, 'breakpoints', null);
} else {
editor.current.setGutterMarker(editor.current.activeLineNo, 'breakpoints', debuggerMark());
}
enableToolbarButtons();
} else if (res.data.status === 'NotConnected') {
pgAdmin.Browser.notifier.alert(
gettext('Debugger Error'),
gettext('Error while toggling breakpoint.')
);
}
})
.catch(function () {
pgAdmin.Browser.notifier.alert(
gettext('Debugger Error'),
gettext('Error while toggling breakpoint.')
);
});
const lineNo = editor.current.getActiveLine();
editor.current.toggleBreakpoint(lineNo);
enableToolbarButtons();
};
const stopDebugging = () => {
@ -522,7 +433,7 @@ export default function DebuggerComponent({ pgAdmin, selectedNodeInfo, panelId,
.then(function (res) {
if (res.data.data.status) {
// Remove active time in the editor
setActiveLine(-1);
editor.current.setActiveLine(-1);
// Clear timeout on stop debugger.
clearTimeout(timeOut);
@ -628,7 +539,7 @@ export default function DebuggerComponent({ pgAdmin, selectedNodeInfo, panelId,
const pollEndExecuteError = (res) => {
params.directDebugger.direct_execution_completed = true;
setActiveLine(-1);
editor.current.setActiveLine(-1);
//Set the notification message to inform the user that execution is
// completed with error.
@ -654,7 +565,7 @@ export default function DebuggerComponent({ pgAdmin, selectedNodeInfo, panelId,
const updateResultAndMessages = (res) => {
if (res.data.data.result != null) {
setActiveLine(-1);
editor.current.setActiveLine(-1);
// Call function to update results information and set result panel focus
eventBus.current.fireEvent(DEBUGGER_EVENTS.SET_RESULTS, res.data.data.col_info, res.data.data.result);
eventBus.current.fireEvent(DEBUGGER_EVENTS.FOCUS_PANEL, PANELS.RESULTS);
@ -725,7 +636,7 @@ export default function DebuggerComponent({ pgAdmin, selectedNodeInfo, panelId,
As Once the EDB procedure execution is completed then we are
not getting any result so we need to ignore the result.
*/
setActiveLine(-1);
editor.current.setActiveLine(-1);
params.directDebugger.direct_execution_completed = true;
params.directDebugger.polling_timeout_idle = true;
@ -852,26 +763,9 @@ export default function DebuggerComponent({ pgAdmin, selectedNodeInfo, panelId,
return breakpoint_list;
};
// Function to get the latest breakpoint information and update the
// gutters of codemirror
// Function to get the latest breakpoint information
const updateBreakpoint = (transId, updateLocalVar = false) => {
let callBackFunc = (br_list) => {
// If there is no break point to clear then we should return from here.
if ((br_list.length == 1) && (br_list[0].linenumber == -1))
return;
let breakpoint_list = getBreakpointList(br_list);
for (let brk_val of breakpoint_list) {
let info = editor.current.lineInfo((brk_val - 1));
if (info.gutterMarkers != undefined) {
editor.current.setGutterMarker((brk_val - 1), 'breakpoints', null);
} else {
editor.current.setGutterMarker((brk_val - 1), 'breakpoints', debuggerMark());
}
}
let callBackFunc = () => {
if (updateLocalVar) {
// Call function to create and update local variables ....
getLocalVariables(params.transId);
@ -976,7 +870,7 @@ export default function DebuggerComponent({ pgAdmin, selectedNodeInfo, panelId,
const updateInfo = (res, transId) => {
if (!params.directDebugger.debug_type && !params.directDebugger.first_time_indirect_debug) {
setLoaderText('');
setActiveLine(-1);
editor.current.setActiveLine(-1);
clearAllBreakpoint(transId);
params.directDebugger.first_time_indirect_debug = true;
@ -987,7 +881,7 @@ export default function DebuggerComponent({ pgAdmin, selectedNodeInfo, panelId,
// If the source is really changed then only update the breakpoint information
updateBreakpointInfo(res, transId);
setActiveLine(res.data.data.result[0].linenumber - 2);
editor.current.setActiveLine(res.data.data.result[0].linenumber - 1);
// Update the stack, local variables and parameters information
setTimeout(function () {
getStackInformation(transId);

View File

@ -16,7 +16,7 @@ import gettext from 'sources/gettext';
import url_for from 'sources/url_for';
import getApiInstance from '../../../../../static/js/api_instance';
import CodeMirror from '../../../../../static/js/components/CodeMirror';
import CodeMirror from '../../../../../static/js/components/ReactCodeMirror';
import { DEBUGGER_EVENTS } from '../DebuggerConstants';
import { DebuggerEventsContext } from './DebuggerComponent';
import { usePgAdmin } from '../../../../../static/js/BrowserComponent';
@ -36,13 +36,6 @@ export default function DebuggerEditor({ getEditor, params }) {
const api = getApiInstance();
function makeMarker() {
let marker = document.createElement('div');
marker.style.color = '#822';
marker.innerHTML = '●';
return marker;
}
function setBreakpoint(lineNo, setType) {
// Make ajax call to set/clear the break point by user
let baseUrl = url_for('debugger.set_breakpoint', {
@ -67,31 +60,6 @@ export default function DebuggerEditor({ getEditor, params }) {
});
}
function onBreakPoint(cm, n, gutter) {
// If breakpoint gutter is clicked and execution is not completed then only set the breakpoint
if (gutter == 'breakpoints' && !params.debuggerDirect.polling_timeout_idle) {
let info = cm.lineInfo(n);
// If gutterMarker is undefined that means there is no marker defined previously
// So we need to set the breakpoint command here...
if (info.gutterMarkers == undefined) {
setBreakpoint(n + 1, 1); //set the breakpoint
} else {
if (info.gutterMarkers.breakpoints == undefined) {
setBreakpoint(n + 1, 1); //set the breakpoint
} else {
setBreakpoint(n + 1, 0); //clear the breakpoint
}
}
// If line folding is defined then gutterMarker will be defined so
// we need to find out 'breakpoints' information
let markers = info.gutterMarkers;
if (markers != undefined && info.gutterMarkers.breakpoints == undefined)
markers = info.gutterMarkers.breakpoints;
cm.setGutterMarker(n, 'breakpoints', markers ? null : makeMarker());
}
}
eventBus.registerListener(DEBUGGER_EVENTS.EDITOR_SET_SQL, (value, focus = true) => {
focus && editor.current?.focus();
editor.current?.setValue(value);
@ -99,19 +67,21 @@ export default function DebuggerEditor({ getEditor, params }) {
useEffect(() => {
self = this;
// Register the callback when user set/clear the breakpoint on gutter area.
editor.current.on('gutterClick', onBreakPoint);
getEditor(editor.current);
}, [editor.current]);
return (
<CodeMirror
currEditor={(obj) => {
editor.current = obj;
}}
gutters={['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'breakpoints']}
value={''}
onBreakPointChange={(line, on)=>{
setBreakpoint(line, on ? 1 : 0);
}}
className={classes.sql}
disabled={true}
readonly={true}
breakpoint
/>);
}

View File

@ -4,7 +4,7 @@
try {
require(
['sources/generated/debugger', 'sources/pgadmin', 'sources/generated/codemirror'],
['sources/generated/debugger', 'sources/pgadmin'],
function(pgDirectDebug, pgAdmin) {
var pgDebug = window.pgAdmin.Tools.Debugger;
pgDebug.load(document.getElementById('debugger-main-container'), {{ uniqueId }}, {{ debug_type }}, '{{ function_name_with_arguments }}', '{{layout|safe}}');

View File

@ -29,7 +29,7 @@
{% block init_script %}
try {
require(
['sources/generated/browser_nodes', 'sources/generated/codemirror'],
['sources/generated/browser_nodes'],
function() {
require(['sources/generated/erd_tool'], function(module) {
window.pgAdmin.Tools.ERD.loadComponent(

View File

@ -108,17 +108,6 @@
height: calc(100% - #{$footer-height-calc});
}
.wizard-right-panel_content .CodeMirror {
border: 1px solid $color-gray-light;
height: 100% !important;
min-height: 100% !important;
}
.wizard-right-panel_content .CodeMirror-linenumber {
background: $color-gray-light;
border-right: none;
}
.grant_wizard_container {
position: relative;
overflow: hidden;

View File

@ -2,7 +2,7 @@
{% block init_script %}
try {
require(
['sources/generated/codemirror', 'sources/generated/browser_nodes', 'sources/generated/schema_diff'],
['sources/generated/browser_nodes', 'sources/generated/schema_diff'],
function() {
var pgSchemaDiff = window.pgAdmin.Tools.SchemaDiff;
pgSchemaDiff.load(document.getElementById('schema-diff-main-container'),{{trans_id}});

View File

@ -10,13 +10,12 @@ import { makeStyles } from '@material-ui/styles';
import React, {useContext, useCallback, useEffect } from 'react';
import { format } from 'sql-formatter';
import { QueryToolContext, QueryToolEventsContext } from '../QueryToolComponent';
import CodeMirror from '../../../../../../static/js/components/CodeMirror';
import CodeMirror from '../../../../../../static/js/components/ReactCodeMirror';
import {PANELS, QUERY_TOOL_EVENTS} from '../QueryToolConstants';
import url_for from 'sources/url_for';
import { LayoutDockerContext, LAYOUT_EVENTS } from '../../../../../../static/js/helpers/Layout';
import ConfirmSaveContent from '../../../../../../static/js/Dialogs/ConfirmSaveContent';
import gettext from 'sources/gettext';
import OrigCodeMirror from 'bundled_codemirror';
import { isMac } from '../../../../../../static/js/keyboard_shortcuts';
import { checkTrojanSource } from '../../../../../../static/js/utils';
import { parseApiError } from '../../../../../../static/js/api_instance';
@ -32,211 +31,32 @@ const useStyles = makeStyles(()=>({
}
}));
function registerAutocomplete(api, transId, sqlEditorPref, onFailure) {
let timeoutId;
let loadingEle;
let prevSearch = null;
OrigCodeMirror.registerHelper('hint', 'sql', function (editor) {
let data = [],
doc = editor.getDoc(),
cur = doc.getCursor(),
// function context
ctx = {
editor: editor,
// URL for auto-complete
url: url_for('sqleditor.autocomplete', {
'trans_id': transId,
}),
data: data,
// Get the line number in the cursor position
current_line: cur.line,
/*
* Render function for hint to add our own class
* and icon as per the object type.
*/
hint_render: function (elt, data_arg, cur_arg) {
let el = document.createElement('span');
switch (cur_arg.type) {
case 'database':
el.className = 'sqleditor-hint pg-icon-' + cur_arg.type;
break;
case 'datatype':
el.className = 'sqleditor-hint icon-type';
break;
case 'keyword':
el.className = 'sqleditor-hint icon-key';
break;
case 'table alias':
el.className = 'sqleditor-hint icon-at';
break;
case 'join':
case 'fk join':
el.className = 'sqleditor-hint icon-join';
break;
default:
el.className = 'sqleditor-hint icon-' + cur_arg.type;
}
el.appendChild(document.createTextNode(cur_arg.text));
elt.appendChild(el);
},
};
data.push(doc.getValue());
if (!editor.state.autoCompleteList)
editor.state.autoCompleteList = [];
// This function is used to show the loading element until response comes.
const showLoading = (editor)=>{
if (editor.getInputField().getAttribute('aria-activedescendant') != null) {
hideLoading();
return;
}
if(!loadingEle) {
let ownerDocument = editor.getInputField().ownerDocument;
loadingEle = ownerDocument.createElement('div');
loadingEle.className = 'CodeMirror-hints';
let iconEle = ownerDocument.createElement('div');
iconEle.className = 'icon-spinner';
iconEle.style.marginTop = '4px';
iconEle.style.marginLeft = '2px';
let spanEle = ownerDocument.createElement('span');
spanEle.innerText = gettext('Loading...');
spanEle.style.marginLeft = '17px';
iconEle.appendChild(spanEle);
loadingEle.appendChild(iconEle);
ownerDocument.body.appendChild(loadingEle);
}
let pos = editor.cursorCoords(true);
loadingEle.style.left = pos.left + 'px';
loadingEle.style.top = pos.bottom + 'px';
loadingEle.style.height = '25px';
};
// This function is used to hide the loading element.
const hideLoading = ()=>{
loadingEle?.parentNode?.removeChild(loadingEle);
loadingEle = null;
};
return {
then: function (cb) {
let self_local = this;
// This function is used to filter the data and call the callback
// function with that filtered data.
function setAutoCompleteData() {
const searchRe = new RegExp('^"{0,1}' + search, 'i');
let filterData = self_local.editor.state.autoCompleteList.filter((item)=>{
return searchRe.test(item.text);
async function registerAutocomplete(editor, api, transId) {
editor.registerAutocomplete((context, onAvailable)=>{
return new Promise((resolve, reject)=>{
const url = url_for('sqleditor.autocomplete', {
'trans_id': transId,
});
const word = context.matchBefore(/\w*/);
const fullSql = context.state.doc.toString();
api.post(url, JSON.stringify([fullSql, fullSql]))
.then((res) => {
onAvailable();
resolve({
from: word.from,
options: Object.keys(res.data.data.result).map((key)=>({
label: key, type: res.data.data.result[key].object_type
})),
validFor: (text, from)=>{
return text.startsWith(fullSql.slice(from));
}
});
cb({
list: filterData,
from: {
line: self_local.current_line,
ch: start,
},
to: {
line: self_local.current_line,
ch: end,
},
});
}
/*
* Below logic find the start and end point
* to replace the selected auto complete suggestion.
*/
let token = self_local.editor.getTokenAt(cur),
start, end, search;
if (token.end > cur.ch) {
token.end = cur.ch;
token.string = token.string.slice(0, cur.ch - token.start);
}
if (token.string.match(/^[."`\w@]\w*$/)) {
search = token.string;
start = token.start;
end = token.end;
} else {
start = end = cur.ch;
search = '';
}
/*
* Added 1 in the start position if search string
* started with "." or "`" else auto complete of code mirror
* will remove the "." when user select any suggestion.
*/
if (search.charAt(0) == '.' || search.charAt(0) == '``') {
start += 1;
search = search.slice(1);
}
// Handled special case when autocomplete on keypress is off,
// the query is cleared, and retype some other words and press CTRL/CMD + Space.
if (!sqlEditorPref.autocomplete_on_key_press && start == 0)
self_local.editor.state.autoCompleteList = [];
// Clear the auto complete list if previous token/search is blank or dot.
if (prevSearch == '' || prevSearch == '.' || prevSearch == '"')
self_local.editor.state.autoCompleteList = [];
prevSearch = search;
// Get the text from start to the current cursor position.
self_local.data.push(
doc.getRange({
line: 0,
ch: 0,
}, {
line: self_local.current_line,
ch: token.start + 1,
})
);
// If search token is not empty and auto complete list have some data
// then no need to send the request to the backend to fetch the data.
// auto complete the data using already fetched list.
if (search != '' && self_local.editor.state.autoCompleteList.length != 0) {
setAutoCompleteData();
return;
}
//Show loading indicator
showLoading(self_local.editor);
timeoutId && clearTimeout(timeoutId);
timeoutId = setTimeout(()=> {
timeoutId = null;
// Make ajax call to find the autocomplete data
api.post(self_local.url, JSON.stringify(self_local.data))
.then((res) => {
hideLoading();
let result = [];
_.each(res.data.data.result, function (obj, key) {
result.push({
text: key,
type: obj.object_type,
render: self_local.hint_render,
});
});
self_local.editor.state.autoCompleteList = result;
setAutoCompleteData();
})
.catch((err) => {
hideLoading();
onFailure?.(err);
});
}, 300);
}.bind(ctx),
};
})
.catch((err) => {
onAvailable();
reject(err);
});
});
});
}
@ -247,31 +67,22 @@ export default function Query() {
const queryToolCtx = useContext(QueryToolContext);
const layoutDocker = useContext(LayoutDockerContext);
const lastCursorPos = React.useRef();
const lastSavedText = React.useRef('');
const markedLine = React.useRef(0);
const marker = React.useRef();
const pgAdmin = usePgAdmin();
const preferencesStore = usePreferences();
const removeHighlightError = (cmObj)=>{
// Remove already existing marker
marker.current?.clear();
cmObj.removeLineClass(markedLine.current, 'wrap', 'CodeMirror-activeline-background');
markedLine.current = 0;
};
const highlightError = (cmObj, {errormsg: result, data})=>{
let errorLineNo = 0,
startMarker = 0,
endMarker = 0,
selectedLineNo = 0,
selectedLineNo = 1,
origQueryLen = cmObj.getValue().length;
removeHighlightError(cmObj);
cmObj.removeErrorMark();
// In case of selection we need to find the actual line no
if (cmObj.getSelection().length > 0) {
selectedLineNo = cmObj.getCursor(true).line;
origQueryLen = cmObj.getLine(selectedLineNo).length;
selectedLineNo = cmObj.getCurrentLineNo();
origQueryLen = cmObj.line(selectedLineNo).length;
}
// Fetch the LINE string using regex from the result
@ -281,8 +92,8 @@ export default function Query() {
// If line and character is null then no need to mark
if (line != null && char != null) {
errorLineNo = (parseInt(line[1]) - 1) + selectedLineNo;
let errorCharNo = (parseInt(char[1]) - 1);
errorLineNo = parseInt(line[1]) + selectedLineNo - 1;
let errorCharNo = parseInt(char[1]) - 1;
/* If explain query has been run we need to
calculate the character number.
@ -298,8 +109,8 @@ export default function Query() {
* have also added 1 per line for the "\n" character.
*/
let prevLineChars = 0;
for (let i = selectedLineNo > 0 ? selectedLineNo : 0; i < errorLineNo; i++)
prevLineChars += cmObj.getLine(i).length + 1;
for (let i = selectedLineNo; i < errorLineNo; i++)
prevLineChars += cmObj.getLine(i).length;
/* Marker starting point for the individual line is
* equal to error character index minus total no of
@ -316,18 +127,14 @@ export default function Query() {
endMarker = errorLine.length;
// Mark the error text
marker.current = cmObj.markText({
cmObj.setErrorMark({
line: errorLineNo,
ch: startMarker,
pos: startMarker,
}, {
line: errorLineNo,
ch: endMarker,
}, {
className: 'sql-editor-mark',
pos: endMarker,
});
markedLine.current = errorLineNo;
cmObj.addLineClass(errorLineNo, 'wrap', 'CodeMirror-activeline-background');
cmObj.focus();
cmObj.setCursor(errorLineNo, endMarker);
}
@ -364,7 +171,7 @@ export default function Query() {
if(result) {
highlightError(editor.current, result);
} else {
removeHighlightError(editor.current);
editor.current.removeErrorMark();
}
});
@ -381,7 +188,7 @@ export default function Query() {
editor.current.setValue(res.data);
//Check the file content for Trojan Source
checkTrojanSource(res.data);
lastSavedText.current = res.data;
editor.current.markClean();
eventBus.fireEvent(QUERY_TOOL_EVENTS.LOAD_FILE_DONE, fileName, true);
}).catch((err)=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.LOAD_FILE_DONE, null, false);
@ -390,12 +197,11 @@ export default function Query() {
});
eventBus.registerListener(QUERY_TOOL_EVENTS.SAVE_FILE, (fileName)=>{
let editorValue = editor.current.getValue();
queryToolCtx.api.post(url_for('sqleditor.save_file'), {
'file_name': decodeURI(fileName),
'file_content': editor.current.getValue(),
}).then(()=>{
lastSavedText.current = editorValue;
editor.current.markClean();
eventBus.fireEvent(QUERY_TOOL_EVENTS.SAVE_FILE_DONE, fileName, true);
pgAdmin.Browser.notifier.success(gettext('File saved successfully.'));
}).catch((err)=>{
@ -426,16 +232,11 @@ export default function Query() {
key.shiftKey = false;
key.altKey = replace;
}
editor.current?.triggerOnKeyDown(
new KeyboardEvent('keydown', key)
);
editor.current?.fireDOMEvent(new KeyboardEvent('keydown', key));
});
eventBus.registerListener(QUERY_TOOL_EVENTS.EDITOR_SET_SQL, (value, focus=true)=>{
focus && editor.current?.focus();
if(!queryToolCtx.params.is_query_tool){
lastSavedText.current = value;
}
editor.current?.setValue(value);
editor.current?.setValue(value, !queryToolCtx.params.is_query_tool);
if (value == '' && editor.current) {
editor.current.state.autoCompleteList = [];
}
@ -500,7 +301,7 @@ export default function Query() {
useEffect(()=>{
const warnSaveTextClose = ()=>{
if(!isDirty() || !queryToolCtx.preferences?.sqleditor.prompt_save_query_changes) {
if(!editor.current.isDirty() || !queryToolCtx.preferences?.sqleditor.prompt_save_query_changes) {
eventBus.fireEvent(QUERY_TOOL_EVENTS.WARN_TXN_CLOSE);
return;
}
@ -526,23 +327,20 @@ export default function Query() {
}, [queryToolCtx.preferences]);
useEffect(()=>{
registerAutocomplete(queryToolCtx.api, queryToolCtx.params.trans_id, queryToolCtx.preferences.sqleditor,
registerAutocomplete(editor.current, queryToolCtx.api, queryToolCtx.params.trans_id, queryToolCtx.preferences.sqleditor,
(err)=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, err);}
);
}, [queryToolCtx.params.trans_id]);
const isDirty = ()=>(lastSavedText.current !== editor.current.getValue());
const cursorActivity = useCallback(_.debounce((cmObj)=>{
const c = cmObj.getCursor();
lastCursorPos.current = c;
eventBus.fireEvent(QUERY_TOOL_EVENTS.CURSOR_ACTIVITY, [c.line+1, c.ch+1]);
const cursorActivity = useCallback(_.debounce((cursor)=>{
lastCursorPos.current = cursor;
eventBus.fireEvent(QUERY_TOOL_EVENTS.CURSOR_ACTIVITY, [lastCursorPos.current.line, lastCursorPos.current.ch+1]);
}, 100), []);
const change = useCallback(()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.QUERY_CHANGED, isDirty());
eventBus.fireEvent(QUERY_TOOL_EVENTS.QUERY_CHANGED, editor.current.isDirty());
if(!queryToolCtx.params.is_query_tool && isDirty()){
if(!queryToolCtx.params.is_query_tool && editor.current.isDirty()){
if(queryToolCtx.preferences.sqleditor.view_edit_promotion_warning){
checkViewEditDataPromotion();
} else {
@ -552,7 +350,7 @@ export default function Query() {
}, []);
const closePromotionWarning = (closeModal)=>{
if(isDirty()) {
if(editor.current.isDirty()) {
editor.current.undo();
closeModal?.();
}
@ -567,7 +365,7 @@ export default function Query() {
promoteToQueryTool();
let cursor = editor.current.getCursor();
editor.current.setValue(editor.current.getValue());
editor.current.setCursor(cursor);
editor.current.setCursor(cursor.line, cursor.ch);
editor.current.focus();
let title = getTitle(pgAdmin, queryToolCtx.preferences.browser, null,null,queryToolCtx.params.server_name, queryToolCtx.params.dbname, queryToolCtx.params.user);
queryToolCtx.updateTitle(title);
@ -599,11 +397,9 @@ export default function Query() {
}}
value={''}
className={classes.sql}
events={{
'focus': cursorActivity,
'cursorActivity': cursorActivity,
'change': change,
}}
onCursorActivity={cursorActivity}
onChange={change}
autocomplete={true}
keepHistory={queryToolCtx.params.is_query_tool}
/>;
}

View File

@ -22,7 +22,7 @@ import AssessmentRoundedIcon from '@material-ui/icons/AssessmentRounded';
import ExplicitRoundedIcon from '@material-ui/icons/ExplicitRounded';
import { SaveDataIcon, CommitIcon, RollbackIcon, ViewDataIcon } from '../../../../../../static/js/components/ExternalIcon';
import { InputSwitch } from '../../../../../../static/js/components/FormComponents';
import CodeMirror from '../../../../../../static/js/components/CodeMirror';
import CodeMirror from '../../../../../../static/js/components/ReactCodeMirror';
import { DefaultButton } from '../../../../../../static/js/components/Buttons';
import { useDelayedCaller } from '../../../../../../static/js/custom_hooks';
import Loader from 'sources/components/Loader';

View File

@ -29,7 +29,7 @@
{% block init_script %}
try {
require(
['sources/generated/browser_nodes', 'sources/generated/codemirror'],
['sources/generated/browser_nodes'],
function() {
require(['sources/generated/sqleditor'], function(module) {
window.pgAdmin.Tools.SQLEditor.loadComponent(

View File

@ -172,8 +172,8 @@ class QueryToolAutoCompleteFeatureTest(BaseFeatureTest):
ActionChains(self.page.driver).key_down(
Keys.CONTROL).send_keys(Keys.SPACE).key_up(
Keys.CONTROL).perform()
if self.page.check_if_element_exist_by_xpath(
QueryToolLocators.code_mirror_hint_box_xpath, 15):
if self.page.check_if_element_exist_by_css_selector(
QueryToolLocators.code_mirror_hint_box, 15):
hint_displayed = True
break
else:
@ -186,12 +186,11 @@ class QueryToolAutoCompleteFeatureTest(BaseFeatureTest):
else:
# if no IntelliSense is present it means there is only one option
# so check if required string is present in codeMirror
code_mirror = self.driver.find_elements(
By.XPATH, QueryToolLocators.code_mirror_data_xpath)
for data in code_mirror:
code_mirror_text = data.text
print("Single entry..........")
if expected_string not in code_mirror_text:
print("single entry exception.........")
raise RuntimeError("Required String %s is not "
"present" % expected_string)
code_mirror_text = self.driver.find_element(
By.CSS_SELECTOR, QueryToolLocators.code_mirror_content
.format('#id-query')).text
if expected_string not in code_mirror_text:
print("single entry exception.........")
raise RuntimeError("Required String %s is not "
"present" % expected_string)

View File

@ -46,7 +46,7 @@ class TableDdlFeatureTest(BaseFeatureTest):
# Wait till data is displayed in SQL Tab
self.assertTrue(self.page.check_if_element_exist_by_xpath(
"//*[contains(@class,'CodeMirror-lines') and "
"//*[contains(@class,'cm-line') and "
"contains(.,'CREATE TABLE IF NOT EXISTS public.%s')]"
% self.test_table_name, 10), "No data displayed in SQL tab")

View File

@ -51,18 +51,14 @@ class CopySQLFeatureTest(BaseFeatureTest):
self.page.click_tab("SQL")
# Wait till data is displayed in SQL Tab
self.assertTrue(self.page.check_if_element_exist_by_xpath(
"//*[contains(@class,'CodeMirror-lines') and "
"//*[contains(@class,'cm-line') and "
"contains(.,'CREATE TABLE IF NOT EXISTS public.%s')]"
% self.test_table_name, 10), "No data displayed in SQL tab")
# Fetch the inner html & check for escaped characters
source_code = self.driver.find_elements(
By.XPATH, QueryToolLocators.code_mirror_data_xpath)
sql_query = ''
for data in source_code:
sql_query += data.text
sql_query += '\n'
sql_query = self.driver.find_element(
By.CSS_SELECTOR, QueryToolLocators.code_mirror_content
.format('#id-sql')).text
return sql_query
@ -76,12 +72,9 @@ class CopySQLFeatureTest(BaseFeatureTest):
self.driver.switch_to.frame(
self.driver.find_element(By.TAG_NAME, "iframe"))
code_mirror = self.driver.find_elements(
By.XPATH, QueryToolLocators.code_mirror_data_xpath)
query_tool_result = ''
for data in code_mirror:
query_tool_result += data.text
query_tool_result += '\n'
query_tool_result = self.driver.find_element(
By.CSS_SELECTOR, QueryToolLocators.code_mirror_content
.format('#id-query')).text
return query_tool_result

View File

@ -128,12 +128,12 @@ class CheckForXssFeatureTest(BaseFeatureTest):
# Wait till data is displayed in SQL Tab
self.assertTrue(self.page.check_if_element_exist_by_xpath(
"//*[contains(@class,'CodeMirror-lines') and "
"//*[contains(@class,'cm-line') and "
"contains(.,'CREATE TABLE')]", 10), "No data displayed in SQL tab")
# Fetch the inner html & check for escaped characters
source_code = self.page.find_by_xpath(
"//*[contains(@class,'CodeMirror-lines') and "
"//*[contains(@class,'cm-line') and "
"contains(.,'CREATE TABLE')]"
).get_attribute('innerHTML')

View File

@ -235,12 +235,13 @@ class QueryToolLocators:
sql_editor_message = "//div[@id='id-messages'][contains(string(), '{}')]"
code_mirror_hint_box_xpath = "//ul[@class='CodeMirror-hints default']"
code_mirror_hint_box = ".cm-editor .cm-tooltip-autocomplete"
code_mirror_hint_item_xpath = \
"//ul[contains(@class, 'CodeMirror-hints') and contains(., '{}')]"
("//div[contains(@class, 'cm-tooltip-autocomplete') "
"and contains(., '{}')]")
code_mirror_data_xpath = "//pre[@class=' CodeMirror-line ']/span"
code_mirror_content = "{0} .cm-content"
btn_commit = "button[data-label='Commit']"

View File

@ -892,7 +892,8 @@ class PgadminPage:
driver.switch_to.frame(
driver.find_element(By.TAG_NAME, "iframe"))
element = driver.find_element(
By.CSS_SELECTOR, "#sqleditor-container .CodeMirror")
By.CSS_SELECTOR,
"#sqleditor-container #id-query .cm-content")
if element.is_displayed() and element.is_enabled():
return element
except (NoSuchElementException, WebDriverException):
@ -933,9 +934,9 @@ class PgadminPage:
action.perform()
else:
self.driver.execute_script(
"arguments[0].CodeMirror.setValue(arguments[1]);"
"arguments[0].CodeMirror.setCursor("
"arguments[0].CodeMirror.lineCount(),0);",
"arguments[0].cmView.view.setValue(arguments[1]);"
"arguments[0].cmView.view.setCursor("
"arguments[0].cmView.view.lineCount(),0);",
codemirror_ele, field_content)
def click_tab(self, tab_name):

View File

@ -168,20 +168,20 @@ describe('SchemaView', ()=>{
it('no changes', async ()=>{
await user.click(ctrl.container.querySelector('button[data-test="SQL"]'));
expect(ctrl.container.querySelector('[data-testid="SQL"] textarea')).toHaveValue('-- No updates.');
expect(ctrl.container.querySelector('[data-testid="SQL"] .cm-content')).toHaveTextContent('-- No updates.');
});
it('data invalid', async ()=>{
await user.clear(ctrl.container.querySelector('[name="field2"]'));
await user.type(ctrl.container.querySelector('[name="field2"]'), '2');
await user.click(ctrl.container.querySelector('button[data-test="SQL"]'));
expect(ctrl.container.querySelector('[data-testid="SQL"] textarea')).toHaveValue('-- Definition incomplete.');
expect(ctrl.container.querySelector('[data-testid="SQL"] .cm-content')).toHaveTextContent('-- Definition incomplete.');
});
it('valid data', async ()=>{
await simulateValidData();
await user.click(ctrl.container.querySelector('button[data-test="SQL"]'));
expect(ctrl.container.querySelector('[data-testid="SQL"] textarea')).toHaveValue('select 1;');
expect(ctrl.container.querySelector('[data-testid="SQL"] .cm-content')).toHaveTextContent('select 1;');
});
});

View File

@ -1,39 +0,0 @@
const getSearchCursorRet = {
_from: 3,
_to: 14,
find: function(_rev) {
if(_rev){
this._from = 1;
this._to = 10;
} else {
this._from = 3;
this._to = 14;
}
return true;
},
from: function() {return this._from;},
to: function() {return this._to;},
replace: jest.fn(),
};
const fromTextAreaRet = {
'getValue':()=>'',
'setValue': jest.fn(),
'refresh': jest.fn(),
'setOption': jest.fn(),
'removeKeyMap': jest.fn(),
'addKeyMap': jest.fn(),
'getSelection': () => '',
'getSearchCursor': jest.fn(()=>getSearchCursorRet),
'getCursor': jest.fn(),
'removeOverlay': jest.fn(),
'addOverlay': jest.fn(),
'setSelection': jest.fn(),
'scrollIntoView': jest.fn(),
'getWrapperElement': ()=>document.createElement('div'),
'on': jest.fn(),
'off': jest.fn(),
'toTextArea': jest.fn(),
};
module.exports = {
fromTextArea: jest.fn(()=>fromTextAreaRet)
};

View File

@ -9,41 +9,54 @@
import React from 'react';
import {default as OrigCodeMirror} from 'bundled_codemirror';
import { withTheme } from '../fake_theme';
import pgWindow from 'sources/window';
import CodeMirror from 'sources/components/CodeMirror';
import { FindDialog } from '../../../pgadmin/static/js/components/CodeMirror';
import CodeMirror from 'sources/components/ReactCodeMirror';
import FindDialog from 'sources/components/ReactCodeMirror/FindDialog';
import CustomEditorView from 'sources/components/ReactCodeMirror/CustomEditorView';
import fakePgAdmin from '../fake_pgadmin';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { render, screen, fireEvent } from '@testing-library/react';
import * as CMSearch from '@codemirror/search';
jest.mock('sources/components/ReactCodeMirror/CustomEditorView');
jest.mock('@codemirror/search', () => ({
...(jest.requireActual('@codemirror/search')),
SearchQuery: jest.fn().mockImplementation(() => {
return {
eq: jest.fn(),
};
}),
openSearchPanel: jest.fn(),
closeSearchPanel: jest.fn(),
replaceNext: jest.fn(),
}));
describe('CodeMirror', ()=>{
const ThemedCM = withTheme(CodeMirror);
let cmInstance, options={
lineNumbers: true,
mode: 'text/x-pgsql',
},
cmObj = OrigCodeMirror.fromTextArea();
let cmInstance, editor;
const cmRerender = (props)=>{
cmInstance.rerender(
<ThemedCM
value={'Init text'}
options={options}
className="testClass"
currEditor={(obj) => {
editor = obj;
}}
{...props}
/>
);
};
beforeEach(()=>{
pgWindow.pgAdmin = fakePgAdmin;
// jest.spyOn(OrigCodeMirror, 'fromTextArea').mockReturnValue(cmObj);
cmInstance = render(
<ThemedCM
value={'Init text'}
options={options}
className="testClass"
currEditor={(obj) => {
editor = obj;
}}
/>);
});
@ -52,17 +65,23 @@ describe('CodeMirror', ()=>{
});
it('init', async ()=>{
/* textarea ref passed to fromTextArea */
expect(OrigCodeMirror.fromTextArea).toHaveBeenCalledWith(cmInstance.container.querySelector('textarea'), expect.objectContaining(options));
await waitFor(() => expect(cmObj.setValue).toHaveBeenCalledWith('Init text'));
expect(CustomEditorView).toHaveBeenCalledTimes(1);
expect(editor.setValue).toHaveBeenCalledWith('Init text');
});
it('change value', ()=>{
editor.state = {
doc: [],
};
editor.setValue.mockClear();
jest.spyOn(editor, 'getValue').mockReturnValue('Init text');
cmRerender({value: 'the new text'});
expect(cmObj.setValue).toHaveBeenCalledWith('the new text');
expect(editor.setValue).toHaveBeenCalledWith('the new text');
editor.setValue.mockClear();
jest.spyOn(editor, 'getValue').mockReturnValue('the new text');
cmRerender({value: null});
expect(cmObj.setValue).toHaveBeenCalledWith('');
expect(editor.setValue).toHaveBeenCalledWith('');
});
@ -73,7 +92,7 @@ describe('CodeMirror', ()=>{
const ctrlMount = (props)=>{
ctrl = render(
<ThemedFindDialog
editor={cmObj}
editor={editor}
show={true}
onClose={onClose}
{...props}
@ -84,29 +103,27 @@ describe('CodeMirror', ()=>{
it('init', ()=>{
ctrlMount({});
cmObj.removeOverlay.mockClear();
cmObj.addOverlay.mockClear();
CMSearch.SearchQuery.mockClear();
const input = ctrl.container.querySelector('input');
fireEvent.change(input, {
target: {value: '\n\r\tA'},
});
expect(cmObj.removeOverlay).toHaveBeenCalled();
expect(cmObj.addOverlay).toHaveBeenCalled();
expect(cmObj.setSelection).toHaveBeenCalledWith(3, 14);
expect(cmObj.scrollIntoView).toHaveBeenCalled();
expect(CMSearch.SearchQuery).toHaveBeenCalledWith(expect.objectContaining({
search: expect.stringContaining('A')
}));
});
it('escape', ()=>{
ctrlMount({});
cmObj.removeOverlay.mockClear();
CMSearch.closeSearchPanel.mockClear();
fireEvent.keyDown(ctrl.container.querySelector('input'), {
key: 'Escape',
});
expect(cmObj.removeOverlay).toHaveBeenCalled();
expect(CMSearch.closeSearchPanel).toHaveBeenCalled();
});
it('toggle match case', ()=>{
@ -132,7 +149,8 @@ describe('CodeMirror', ()=>{
it('replace', async ()=>{
ctrlMount({replace: true});
cmObj.getSearchCursor().replace.mockClear();
CMSearch.SearchQuery.mockClear();
fireEvent.change(ctrl.container.querySelectorAll('input')[0], {
target: {value: 'A'},
});
@ -142,9 +160,13 @@ describe('CodeMirror', ()=>{
fireEvent.keyPress(ctrl.container.querySelectorAll('input')[1], {
key: 'Enter', shiftKey: true, code: 13, charCode: 13
});
await waitFor(()=>{
expect(cmObj.getSearchCursor().replace).toHaveBeenCalled();
});
expect(CMSearch.SearchQuery).toHaveBeenCalledWith(expect.objectContaining({
search: 'A',
replace: 'B'
}));
expect(CMSearch.replaceNext).toHaveBeenCalled();
});
});
});

View File

@ -50,6 +50,20 @@ global.afterEach(() => {
window.HTMLElement.prototype.scrollIntoView = function() {};
// required for Codemirror 6 to run in jsdom
document.createRange = () => {
const range = new Range();
range.getBoundingClientRect = jest.fn();
range.getClientRects = jest.fn(() => ({
item: () => null,
length: 0,
}));
return range;
};
jest.setTimeout(15000); // 1 second

View File

@ -357,7 +357,6 @@ module.exports = [{
// Specify entry points of application
entry: {
'app.bundle': sourceDir + '/bundle/app.js',
codemirror: sourceDir + '/bundle/codemirror.js',
'security.pages': 'security.pages',
sqleditor: './pgadmin/tools/sqleditor/static/js/index.js',
schema_diff: './pgadmin/tools/schema_diff/static/js/index.js',

View File

@ -22,7 +22,6 @@ let webpackShimConfig = {
// used by webpack while creating bundle
resolveAlias: {
'top': path.join(__dirname, './pgadmin/'),
'bundled_codemirror': path.join(__dirname, './pgadmin/static/bundle/codemirror'),
'bundled_browser': path.join(__dirname, './pgadmin/static/bundle/browser'),
'sources': path.join(__dirname, './pgadmin/static/js/'),
'translations': path.join(__dirname, './pgadmin/tools/templates/js/translations'),

View File

@ -2354,6 +2354,103 @@ __metadata:
languageName: node
linkType: hard
"@codemirror/autocomplete@npm:^6.0.0":
version: 6.12.0
resolution: "@codemirror/autocomplete@npm:6.12.0"
dependencies:
"@codemirror/language": ^6.0.0
"@codemirror/state": ^6.0.0
"@codemirror/view": ^6.17.0
"@lezer/common": ^1.0.0
peerDependencies:
"@codemirror/language": ^6.0.0
"@codemirror/state": ^6.0.0
"@codemirror/view": ^6.0.0
"@lezer/common": ^1.0.0
checksum: 1d4da6ccc12f5a67053a76b361f2683b5af031dd405a0bd2a261a265eb8cb7dfb115343a3291260d1ba31ce7ccb5427208ebe50f50f6747fcf27a50b62c87f7e
languageName: node
linkType: hard
"@codemirror/commands@npm:^6.0.0":
version: 6.3.3
resolution: "@codemirror/commands@npm:6.3.3"
dependencies:
"@codemirror/language": ^6.0.0
"@codemirror/state": ^6.4.0
"@codemirror/view": ^6.0.0
"@lezer/common": ^1.1.0
checksum: 7d23aecc973823969434b839aefa9a98bb47212d2ce0e6869ae903adbb5233aad22a760788fb7bb6eb45b00b01a4932fb93ad43bacdcbc0215e7500cf54b17bb
languageName: node
linkType: hard
"@codemirror/lang-sql@npm:^6.5.5":
version: 6.5.5
resolution: "@codemirror/lang-sql@npm:6.5.5"
dependencies:
"@codemirror/autocomplete": ^6.0.0
"@codemirror/language": ^6.0.0
"@codemirror/state": ^6.0.0
"@lezer/common": ^1.2.0
"@lezer/highlight": ^1.0.0
"@lezer/lr": ^1.0.0
checksum: 404003ae73b986bd7a00f6924db78b7ffb28fdc38d689fdc11416aaafe2d5c6dc37cc18972530f82e940acb61e18ac74a1cf7712beef448c145344ff93970dc3
languageName: node
linkType: hard
"@codemirror/language@npm:^6.0.0":
version: 6.10.0
resolution: "@codemirror/language@npm:6.10.0"
dependencies:
"@codemirror/state": ^6.0.0
"@codemirror/view": ^6.23.0
"@lezer/common": ^1.1.0
"@lezer/highlight": ^1.0.0
"@lezer/lr": ^1.0.0
style-mod: ^4.0.0
checksum: 3bfd9968f5a34ce22434489a5b226db5f3bc454a1ae7c4381587ff4270ac6af61b10f93df560cb73e9a77cc13d4843722a7a7b94dbed02a3ab1971dd329b9e81
languageName: node
linkType: hard
"@codemirror/lint@npm:^6.0.0":
version: 6.4.2
resolution: "@codemirror/lint@npm:6.4.2"
dependencies:
"@codemirror/state": ^6.0.0
"@codemirror/view": ^6.0.0
crelt: ^1.0.5
checksum: 5e699960c1b28dbaa584fe091a3201978907bf4b9e52810fb15d3ceaf310e38053435e0b594da0985266ae812039a5cd6c36023284a6f8568664bdca04db137f
languageName: node
linkType: hard
"@codemirror/search@npm:^6.0.0":
version: 6.5.5
resolution: "@codemirror/search@npm:6.5.5"
dependencies:
"@codemirror/state": ^6.0.0
"@codemirror/view": ^6.0.0
crelt: ^1.0.5
checksum: 825196ef63273494ba9a6153b01eda385edb65e77a1e49980dd3a28d4a692af1e9575e03e4b6c84f6fa2afe72217113ff4c50f58b20d13fe0d277cda5dd7dc81
languageName: node
linkType: hard
"@codemirror/state@npm:^6.0.0, @codemirror/state@npm:^6.4.0":
version: 6.4.0
resolution: "@codemirror/state@npm:6.4.0"
checksum: c5236fe5786f1b85d17273a5c17fa8aeb063658c1404ab18caeb6e7591663ec96b65d453ab8162f75839c3801b04cd55ba4bc48f44cb61ebfeeee383f89553c7
languageName: node
linkType: hard
"@codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0":
version: 6.23.1
resolution: "@codemirror/view@npm:6.23.1"
dependencies:
"@codemirror/state": ^6.4.0
style-mod: ^4.1.0
w3c-keyname: ^2.2.4
checksum: 5ea3ba5761c574e1f6e1f1058cb452189c890982a77991606d0ae40da3c6fff77f7c7fc3c43fa78d62677ccdfa65dbc56175706b793e34ad4ec7a63b21e8c18e
languageName: node
linkType: hard
"@date-io/core@npm:1.x, @date-io/core@npm:^1.3.13, @date-io/core@npm:^1.3.6":
version: 1.3.13
resolution: "@date-io/core@npm:1.3.13"
@ -3086,6 +3183,31 @@ __metadata:
languageName: node
linkType: hard
"@lezer/common@npm:^1.0.0, @lezer/common@npm:^1.1.0, @lezer/common@npm:^1.2.0":
version: 1.2.1
resolution: "@lezer/common@npm:1.2.1"
checksum: 0bd092e293a509ce334f4aaf9a4d4a25528f743cd9d7e7948c697e34ac703b805b288b62ad01563488fb206fc34ff05084f7fc5d864be775924b3d0d53ea5dd2
languageName: node
linkType: hard
"@lezer/highlight@npm:^1.0.0":
version: 1.2.0
resolution: "@lezer/highlight@npm:1.2.0"
dependencies:
"@lezer/common": ^1.0.0
checksum: 5b9dfe741f95db13f6124cb9556a43011cb8041ecf490be98d44a86b04d926a66e912bcd3a766f6a3d79e064410f1a2f60ab240b50b645a12c56987bf4870086
languageName: node
linkType: hard
"@lezer/lr@npm:^1.0.0":
version: 1.4.0
resolution: "@lezer/lr@npm:1.4.0"
dependencies:
"@lezer/common": ^1.0.0
checksum: 4c8517017e9803415c6c5cb8230d8764107eafd7d0b847676cd1023abb863a4b268d0d01c7ce3cf1702c4749527c68f0a26b07c329cb7b68c36ed88362d7b193
languageName: node
linkType: hard
"@material-ui/core@npm:4.12.4":
version: 4.12.4
resolution: "@material-ui/core@npm:4.12.4"
@ -6147,10 +6269,18 @@ __metadata:
languageName: node
linkType: hard
"codemirror@npm:^5.59.2":
version: 5.65.13
resolution: "codemirror@npm:5.65.13"
checksum: 47060461edaebecd03b3fba4e73a30cdccc0c51ce3a3a05bafae3c9cafd682101383e94d77d54081eaf1ae18da5b74343e98343c637c52cea409956469039098
"codemirror@npm:^6.0.1":
version: 6.0.1
resolution: "codemirror@npm:6.0.1"
dependencies:
"@codemirror/autocomplete": ^6.0.0
"@codemirror/commands": ^6.0.0
"@codemirror/language": ^6.0.0
"@codemirror/lint": ^6.0.0
"@codemirror/search": ^6.0.0
"@codemirror/state": ^6.0.0
"@codemirror/view": ^6.0.0
checksum: 1a78f7077ac5801bdbff162aa0c61bf2b974603c7e9a477198c3ce50c789af674a061d7c293c58b73807eda345c2b5228c38ad2aabb9319d552d5486f785cbef
languageName: node
linkType: hard
@ -6504,6 +6634,13 @@ __metadata:
languageName: node
linkType: hard
"crelt@npm:^1.0.5":
version: 1.0.6
resolution: "crelt@npm:1.0.6"
checksum: dad842093371ad702afbc0531bfca2b0a8dd920b23a42f26e66dabbed9aad9acd5b9030496359545ef3937c3aced0fd4ac39f7a2d280a23ddf9eb7fdcb94a69f
languageName: node
linkType: hard
"cross-env@npm:^7.0.3":
version: 7.0.3
resolution: "cross-env@npm:7.0.3"
@ -14809,6 +14946,7 @@ __metadata:
"@babel/preset-env": ^7.10.2
"@babel/preset-react": ^7.12.13
"@babel/preset-typescript": ^7.22.5
"@codemirror/lang-sql": ^6.5.5
"@date-io/core": ^1.3.6
"@date-io/date-fns": 1.x
"@emotion/core": ^10.0.14
@ -14852,7 +14990,7 @@ __metadata:
chartjs-plugin-zoom: ^2.0.1
classnames: ^2.2.6
closest: ^0.0.1
codemirror: ^5.59.2
codemirror: ^6.0.1
convert-units: ^2.3.4
copy-webpack-plugin: ^11.0.0
cross-env: ^7.0.3
@ -15888,6 +16026,13 @@ __metadata:
languageName: node
linkType: hard
"style-mod@npm:^4.0.0, style-mod@npm:^4.1.0":
version: 4.1.0
resolution: "style-mod@npm:4.1.0"
checksum: 8402b14ca11113a3640d46b3cf7ba49f05452df7846bc5185a3535d9b6a64a3019e7fb636b59ccbb7816aeb0725b24723e77a85b05612a9360e419958e13b4e6
languageName: node
linkType: hard
"style-to-js@npm:1.1.9":
version: 1.1.9
resolution: "style-to-js@npm:1.1.9"
@ -16916,6 +17061,13 @@ __metadata:
languageName: node
linkType: hard
"w3c-keyname@npm:^2.2.4":
version: 2.2.8
resolution: "w3c-keyname@npm:2.2.8"
checksum: 95bafa4c04fa2f685a86ca1000069c1ec43ace1f8776c10f226a73296caeddd83f893db885c2c220ebeb6c52d424e3b54d7c0c1e963bbf204038ff1a944fbb07
languageName: node
linkType: hard
"w3c-xmlserializer@npm:^4.0.0":
version: 4.0.0
resolution: "w3c-xmlserializer@npm:4.0.0"