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": { "dependencies": {
"@babel/plugin-proposal-class-properties": "^7.10.4", "@babel/plugin-proposal-class-properties": "^7.10.4",
"@babel/preset-react": "^7.12.13", "@babel/preset-react": "^7.12.13",
"@codemirror/lang-sql": "^6.5.5",
"@date-io/core": "^1.3.6", "@date-io/core": "^1.3.6",
"@date-io/date-fns": "1.x", "@date-io/date-fns": "1.x",
"@emotion/sheet": "^1.0.1", "@emotion/sheet": "^1.0.1",
@ -102,7 +103,7 @@
"chartjs-plugin-zoom": "^2.0.1", "chartjs-plugin-zoom": "^2.0.1",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"closest": "^0.0.1", "closest": "^0.0.1",
"codemirror": "^5.59.2", "codemirror": "^6.0.1",
"convert-units": "^2.3.4", "convert-units": "^2.3.4",
"cssnano": "^5.0.2", "cssnano": "^5.0.2",
"dagre": "^0.8.4", "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", "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-once": "yarn run linter && yarn run jest --maxWorkers=50%",
"test:js": "yarn run test:js-once --watch", "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:js-coverage": "yarn run test:js-once --collect-coverage",
"test:feature": "yarn run bundle && python regression/runtests.py --pkg feature_tests", "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", "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 { div[role=tabpanel] > .pgadmin-control-group.form-group.c.jscexceptions {
min-height: 400px; 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'; import checkNodeVisibility from '../../../static/js/check_node_visibility';
define('pgadmin.browser', [ define('pgadmin.browser', [
'sources/gettext', 'sources/url_for', 'sources/pgadmin', 'bundled_codemirror', 'sources/gettext', 'sources/url_for', 'sources/pgadmin',
'sources/csrf', 'pgadmin.authenticate.kerberos', 'sources/csrf', 'pgadmin.authenticate.kerberos',
'pgadmin.browser.utils', 'pgadmin.browser.messages', 'pgadmin.browser.utils', 'pgadmin.browser.messages',
'pgadmin.browser.node', 'pgadmin.browser.collection', 'pgadmin.browser.activity', 'pgadmin.browser.node', 'pgadmin.browser.collection', 'pgadmin.browser.activity',
'sources/codemirror/addon/fold/pgadmin-sqlfoldcode',
'pgadmin.browser.keyboard', 'sources/tree/pgadmin_tree_save_state', 'pgadmin.browser.keyboard', 'sources/tree/pgadmin_tree_save_state',
], function( ], 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 pgBrowser = pgAdmin.Browser = pgAdmin.Browser || {};
let select_object_msg = gettext('Please select an object in the tree view.'); 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 // Use spaces instead of tab
if (pgBrowser.utils.useSpaces == 'True') { if (pgBrowser.utils.useSpaces == 'True') {
pgAdmin.Browser.editor_shortcut_keys.Tab = 'insertSoftTab'; pgAdmin.Browser.editor_shortcut_keys.Tab = 'insertSoftTab';

View File

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

View File

@ -1,7 +1,7 @@
{% if file_name is not defined %} {% if file_name is not defined %}
{% set file_name=node_type %} {% set file_name=node_type %}
{% endif %} {% 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-image: url('{{ url_for('NODE-%s.static' % node_type, filename='img/%s.svg' % file_name )}}') !important;
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: 20px !important; background-size: 20px !important;

View File

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

View File

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

View File

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

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/@fortawesome/fontawesome-free/css/all.css';
@import 'node_modules/leaflet/dist/leaflet.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/xterm/css/xterm.css';
@import 'node_modules/jsoneditor/dist/jsoneditor.min.css'; @import 'node_modules/jsoneditor/dist/jsoneditor.min.css';

View File

@ -111,7 +111,27 @@ export default function(basicSettings) {
diffColorFg: '#d4d4d4', diffColorFg: '#d4d4d4',
diffSelectFG: '#d4d4d4', diffSelectFG: '#d4d4d4',
diffSelCheckbox: '#323E43' 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', diffColorFg: '#FFFFFF',
diffSelectFG: '#010B15', diffSelectFG: '#010B15',
diffSelCheckbox: '#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 pickrOverride from './overrides/pickr.override';
import uplotOverride from './overrides/uplot.override'; import uplotOverride from './overrides/uplot.override';
import rcdockOverride from './overrides/rcdock.override'; import rcdockOverride from './overrides/rcdock.override';
import cmOverride from './overrides/codemirror.override';
/* Common settings across all themes */ /* Common settings across all themes */
let basicSettings = createTheme(); let basicSettings = createTheme();
@ -333,6 +334,7 @@ function getFinalTheme(baseTheme) {
...pickrOverride(baseTheme), ...pickrOverride(baseTheme),
...uplotOverride(baseTheme), ...uplotOverride(baseTheme),
...rcdockOverride(baseTheme), ...rcdockOverride(baseTheme),
...cmOverride(baseTheme)
}, },
}, },
MuiOutlinedInput: { 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', diffColorFg: '#222',
diffSelectFG: '#222', diffSelectFG: '#222',
diffSelCheckbox: '#d6effc' 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 DateFnsUtils from '@date-io/date-fns';
import * as DateFns from 'date-fns'; import * as DateFns from 'date-fns';
import CodeMirror from './CodeMirror'; import CodeMirror from './ReactCodeMirror';
import gettext from 'sources/gettext'; import gettext from 'sources/gettext';
import _ from 'lodash'; import _ from 'lodash';
import { DefaultButton, PrimaryButton, PgIconButton } from './Buttons'; 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 */ /* 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 classes = useStyles();
const cid = testcid || _.uniqueId('c'); const cid = testcid || _.uniqueId('c');
const helpid = `h${cid}`; const helpid = `h${cid}`;
@ -135,7 +135,7 @@ export function FormInput({ children, error, className, label, helpMessage, requ
return ( return (
<> <>
<Grid item lg={labelGridBasis} md={labelGridBasis} sm={12} xs={12}> <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} {label}
<FormIcon type={MESSAGE_TYPE.ERROR} style={{ marginLeft: 'auto', visibility: error ? 'unset' : 'hidden' }} /> <FormIcon type={MESSAGE_TYPE.ERROR} style={{ marginLeft: 'auto', visibility: error ? 'unset' : 'hidden' }} />
</InputLabel> </InputLabel>
@ -152,7 +152,7 @@ export function FormInput({ children, error, className, label, helpMessage, requ
return ( return (
<Grid container spacing={0} className={className} data-testid="form-input"> <Grid container spacing={0} className={className} data-testid="form-input">
<Grid item lg={labelGridBasis} md={labelGridBasis} sm={12} xs={12}> <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} {label}
<FormIcon type={MESSAGE_TYPE.ERROR} style={{ marginLeft: 'auto', visibility: error ? 'unset' : 'hidden' }} /> <FormIcon type={MESSAGE_TYPE.ERROR} style={{ marginLeft: 'auto', visibility: error ? 'unset' : 'hidden' }} />
</InputLabel> </InputLabel>
@ -174,6 +174,7 @@ FormInput.propTypes = {
helpMessage: PropTypes.string, helpMessage: PropTypes.string,
required: PropTypes.bool, required: PropTypes.bool,
testcid: PropTypes.any, testcid: PropTypes.any,
lid: PropTypes.any,
withContainer: PropTypes.bool, withContainer: PropTypes.bool,
labelGridBasis: PropTypes.number, labelGridBasis: PropTypes.number,
controlGridBasis: PropTypes.number, controlGridBasis: PropTypes.number,
@ -191,16 +192,10 @@ export function InputSQL({ value, options, onChange, className, controlProps, in
}} }}
value={value || ''} value={value || ''}
options={{ options={{
lineNumbers: true,
mode: 'text/x-pgsql',
...options, ...options,
}} }}
className={clsx(classes.sql, className)} className={clsx(classes.sql, className)}
events={{ onChange={onChange}
change: (cm) => {
onChange?.(cm.getValue());
},
}}
{...controlProps} {...controlProps}
{...props} {...props}
/> />
@ -220,9 +215,10 @@ export function FormInputSQL({ hasError, required, label, className, helpMessage
if (noLabel) { if (noLabel) {
return <InputSQL value={value} options={controlProps} {...props} />; return <InputSQL value={value} options={controlProps} {...props} />;
} else { } else {
const lid = _.uniqueId('l');
return ( return (
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid} > <FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid} lid={lid}>
<InputSQL value={value} options={controlProps} {...props} /> <InputSQL value={value} options={controlProps} labelledBy={lid} {...props} />
</FormInput> </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 { .filter-textarea {
height: 100%; height: 100%;
& .CodeMirror-scroll {
min-height: 120px;
max-height: 120px;
}
} }
.dataview_filter_dialog { .dataview_filter_dialog {

View File

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

View File

@ -114,13 +114,13 @@ export default function DebuggerComponent({ pgAdmin, selectedNodeInfo, panelId,
url: baseUrl, url: baseUrl,
method: 'POST', method: 'POST',
data: { data: {
'breakpoint_list': breakpoint_list.lenght > 0 ? breakpoint_list.join() : null, 'breakpoint_list': breakpoint_list.length > 0 ? breakpoint_list.join() : null,
}, },
}) })
.then(function (res) { .then(function (res) {
if (res.data.data.status) { if (res.data.data.status) {
executeQuery(transId); executeQuery(transId);
setUnsetBreakpoint(res, breakpoint_list); editor.current.clearBreakpoints();
} }
enableToolbarButtons(); enableToolbarButtons();
}) })
@ -158,7 +158,8 @@ export default function DebuggerComponent({ pgAdmin, selectedNodeInfo, panelId,
} }
}; };
const raisePollingError = () => { const raisePollingError = (error) => {
console.error(error);
pgAdmin.Browser.notifier.alert( pgAdmin.Browser.notifier.alert(
gettext('Debugger Error'), gettext('Debugger Error'),
gettext('Error while polling result.') 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); 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 .... // Call function to create and update Stack information ....
getStackInformation(transId); 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( pgAdmin.Browser.notifier.alert(
gettext('Debugger Error'), gettext('Debugger Error'),
gettext('Error while executing requested debugging information.') 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) => { const selectFrame = (frameId) => {
// Make ajax call to listen the database message // Make ajax call to listen the database message
let baseUrl = url_for('debugger.select_frame', { let baseUrl = url_for('debugger.select_frame', {
@ -324,7 +301,7 @@ export default function DebuggerComponent({ pgAdmin, selectedNodeInfo, panelId,
if (res.data.data.status) { if (res.data.data.status) {
editor.current.setValue(res.data.data.result[0].src); editor.current.setValue(res.data.data.result[0].src);
updateBreakpoint(params.transId, true); updateBreakpoint(params.transId, true);
setActiveLine(res.data.data.result[0].linenumber - 2); editor.current.setActiveLine(res.data.data.result[0].linenumber - 1);
} }
}) })
.catch(function () { .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 = () => { const triggerClearBreakpoint = () => {
let clearBreakpoint = (br_list) => { let clearBreakpoint = (br_list) => {
// If there is no break point to clear then we should return from here. // 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(), 'breakpoint_list': breakpoint_list.join(),
}, },
}) })
.then(function (res) { .then(function () {
setUnsetBreakpoint(res, breakpoint_list); editor.current.clearBreakpoints();
enableToolbarButtons(); enableToolbarButtons();
}) })
.catch(raiseClearBrekpointError); .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 = () => { const triggerToggleBreakpoint = () => {
disableToolbarButtons(); disableToolbarButtons();
let info = editor.current.lineInfo(editor.current.activeLineNo); const lineNo = editor.current.getActiveLine();
let baseUrl = ''; editor.current.toggleBreakpoint(lineNo);
enableToolbarButtons();
// 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 stopDebugging = () => { const stopDebugging = () => {
@ -522,7 +433,7 @@ export default function DebuggerComponent({ pgAdmin, selectedNodeInfo, panelId,
.then(function (res) { .then(function (res) {
if (res.data.data.status) { if (res.data.data.status) {
// Remove active time in the editor // Remove active time in the editor
setActiveLine(-1); editor.current.setActiveLine(-1);
// Clear timeout on stop debugger. // Clear timeout on stop debugger.
clearTimeout(timeOut); clearTimeout(timeOut);
@ -628,7 +539,7 @@ export default function DebuggerComponent({ pgAdmin, selectedNodeInfo, panelId,
const pollEndExecuteError = (res) => { const pollEndExecuteError = (res) => {
params.directDebugger.direct_execution_completed = true; params.directDebugger.direct_execution_completed = true;
setActiveLine(-1); editor.current.setActiveLine(-1);
//Set the notification message to inform the user that execution is //Set the notification message to inform the user that execution is
// completed with error. // completed with error.
@ -654,7 +565,7 @@ export default function DebuggerComponent({ pgAdmin, selectedNodeInfo, panelId,
const updateResultAndMessages = (res) => { const updateResultAndMessages = (res) => {
if (res.data.data.result != null) { if (res.data.data.result != null) {
setActiveLine(-1); editor.current.setActiveLine(-1);
// Call function to update results information and set result panel focus // 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.SET_RESULTS, res.data.data.col_info, res.data.data.result);
eventBus.current.fireEvent(DEBUGGER_EVENTS.FOCUS_PANEL, PANELS.RESULTS); 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 As Once the EDB procedure execution is completed then we are
not getting any result so we need to ignore the result. 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.direct_execution_completed = true;
params.directDebugger.polling_timeout_idle = true; params.directDebugger.polling_timeout_idle = true;
@ -852,26 +763,9 @@ export default function DebuggerComponent({ pgAdmin, selectedNodeInfo, panelId,
return breakpoint_list; return breakpoint_list;
}; };
// Function to get the latest breakpoint information and update the // Function to get the latest breakpoint information
// gutters of codemirror
const updateBreakpoint = (transId, updateLocalVar = false) => { const updateBreakpoint = (transId, updateLocalVar = false) => {
let callBackFunc = (br_list) => { let callBackFunc = () => {
// 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());
}
}
if (updateLocalVar) { if (updateLocalVar) {
// Call function to create and update local variables .... // Call function to create and update local variables ....
getLocalVariables(params.transId); getLocalVariables(params.transId);
@ -976,7 +870,7 @@ export default function DebuggerComponent({ pgAdmin, selectedNodeInfo, panelId,
const updateInfo = (res, transId) => { const updateInfo = (res, transId) => {
if (!params.directDebugger.debug_type && !params.directDebugger.first_time_indirect_debug) { if (!params.directDebugger.debug_type && !params.directDebugger.first_time_indirect_debug) {
setLoaderText(''); setLoaderText('');
setActiveLine(-1); editor.current.setActiveLine(-1);
clearAllBreakpoint(transId); clearAllBreakpoint(transId);
params.directDebugger.first_time_indirect_debug = true; 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 // If the source is really changed then only update the breakpoint information
updateBreakpointInfo(res, transId); 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 // Update the stack, local variables and parameters information
setTimeout(function () { setTimeout(function () {
getStackInformation(transId); getStackInformation(transId);

View File

@ -16,7 +16,7 @@ import gettext from 'sources/gettext';
import url_for from 'sources/url_for'; import url_for from 'sources/url_for';
import getApiInstance from '../../../../../static/js/api_instance'; 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 { DEBUGGER_EVENTS } from '../DebuggerConstants';
import { DebuggerEventsContext } from './DebuggerComponent'; import { DebuggerEventsContext } from './DebuggerComponent';
import { usePgAdmin } from '../../../../../static/js/BrowserComponent'; import { usePgAdmin } from '../../../../../static/js/BrowserComponent';
@ -36,13 +36,6 @@ export default function DebuggerEditor({ getEditor, params }) {
const api = getApiInstance(); const api = getApiInstance();
function makeMarker() {
let marker = document.createElement('div');
marker.style.color = '#822';
marker.innerHTML = '●';
return marker;
}
function setBreakpoint(lineNo, setType) { function setBreakpoint(lineNo, setType) {
// Make ajax call to set/clear the break point by user // Make ajax call to set/clear the break point by user
let baseUrl = url_for('debugger.set_breakpoint', { 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) => { eventBus.registerListener(DEBUGGER_EVENTS.EDITOR_SET_SQL, (value, focus = true) => {
focus && editor.current?.focus(); focus && editor.current?.focus();
editor.current?.setValue(value); editor.current?.setValue(value);
@ -99,19 +67,21 @@ export default function DebuggerEditor({ getEditor, params }) {
useEffect(() => { useEffect(() => {
self = this; self = this;
// Register the callback when user set/clear the breakpoint on gutter area.
editor.current.on('gutterClick', onBreakPoint);
getEditor(editor.current); getEditor(editor.current);
}, [editor.current]); }, [editor.current]);
return ( return (
<CodeMirror <CodeMirror
currEditor={(obj) => { currEditor={(obj) => {
editor.current = obj; editor.current = obj;
}} }}
gutters={['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'breakpoints']}
value={''} value={''}
onBreakPointChange={(line, on)=>{
setBreakpoint(line, on ? 1 : 0);
}}
className={classes.sql} className={classes.sql}
disabled={true} readonly={true}
breakpoint
/>); />);
} }

View File

@ -4,7 +4,7 @@
try { try {
require( require(
['sources/generated/debugger', 'sources/pgadmin', 'sources/generated/codemirror'], ['sources/generated/debugger', 'sources/pgadmin'],
function(pgDirectDebug, pgAdmin) { function(pgDirectDebug, pgAdmin) {
var pgDebug = window.pgAdmin.Tools.Debugger; var pgDebug = window.pgAdmin.Tools.Debugger;
pgDebug.load(document.getElementById('debugger-main-container'), {{ uniqueId }}, {{ debug_type }}, '{{ function_name_with_arguments }}', '{{layout|safe}}'); 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 %} {% block init_script %}
try { try {
require( require(
['sources/generated/browser_nodes', 'sources/generated/codemirror'], ['sources/generated/browser_nodes'],
function() { function() {
require(['sources/generated/erd_tool'], function(module) { require(['sources/generated/erd_tool'], function(module) {
window.pgAdmin.Tools.ERD.loadComponent( window.pgAdmin.Tools.ERD.loadComponent(

View File

@ -108,17 +108,6 @@
height: calc(100% - #{$footer-height-calc}); 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 { .grant_wizard_container {
position: relative; position: relative;
overflow: hidden; overflow: hidden;

View File

@ -2,7 +2,7 @@
{% block init_script %} {% block init_script %}
try { try {
require( require(
['sources/generated/codemirror', 'sources/generated/browser_nodes', 'sources/generated/schema_diff'], ['sources/generated/browser_nodes', 'sources/generated/schema_diff'],
function() { function() {
var pgSchemaDiff = window.pgAdmin.Tools.SchemaDiff; var pgSchemaDiff = window.pgAdmin.Tools.SchemaDiff;
pgSchemaDiff.load(document.getElementById('schema-diff-main-container'),{{trans_id}}); 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 React, {useContext, useCallback, useEffect } from 'react';
import { format } from 'sql-formatter'; import { format } from 'sql-formatter';
import { QueryToolContext, QueryToolEventsContext } from '../QueryToolComponent'; 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 {PANELS, QUERY_TOOL_EVENTS} from '../QueryToolConstants';
import url_for from 'sources/url_for'; import url_for from 'sources/url_for';
import { LayoutDockerContext, LAYOUT_EVENTS } from '../../../../../../static/js/helpers/Layout'; import { LayoutDockerContext, LAYOUT_EVENTS } from '../../../../../../static/js/helpers/Layout';
import ConfirmSaveContent from '../../../../../../static/js/Dialogs/ConfirmSaveContent'; import ConfirmSaveContent from '../../../../../../static/js/Dialogs/ConfirmSaveContent';
import gettext from 'sources/gettext'; import gettext from 'sources/gettext';
import OrigCodeMirror from 'bundled_codemirror';
import { isMac } from '../../../../../../static/js/keyboard_shortcuts'; import { isMac } from '../../../../../../static/js/keyboard_shortcuts';
import { checkTrojanSource } from '../../../../../../static/js/utils'; import { checkTrojanSource } from '../../../../../../static/js/utils';
import { parseApiError } from '../../../../../../static/js/api_instance'; import { parseApiError } from '../../../../../../static/js/api_instance';
@ -32,211 +31,32 @@ const useStyles = makeStyles(()=>({
} }
})); }));
function registerAutocomplete(api, transId, sqlEditorPref, onFailure) { async function registerAutocomplete(editor, api, transId) {
let timeoutId; editor.registerAutocomplete((context, onAvailable)=>{
let loadingEle; return new Promise((resolve, reject)=>{
let prevSearch = null; const url = url_for('sqleditor.autocomplete', {
OrigCodeMirror.registerHelper('hint', 'sql', function (editor) { 'trans_id': transId,
let data = [], });
doc = editor.getDoc(), const word = context.matchBefore(/\w*/);
cur = doc.getCursor(), const fullSql = context.state.doc.toString();
// function context api.post(url, JSON.stringify([fullSql, fullSql]))
ctx = { .then((res) => {
editor: editor, onAvailable();
// URL for auto-complete resolve({
url: url_for('sqleditor.autocomplete', { from: word.from,
'trans_id': transId, options: Object.keys(res.data.data.result).map((key)=>({
}), label: key, type: res.data.data.result[key].object_type
data: data, })),
// Get the line number in the cursor position validFor: (text, from)=>{
current_line: cur.line, return text.startsWith(fullSql.slice(from));
/* }
* 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);
}); });
})
cb({ .catch((err) => {
list: filterData, onAvailable();
from: { reject(err);
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),
};
}); });
} }
@ -247,31 +67,22 @@ export default function Query() {
const queryToolCtx = useContext(QueryToolContext); const queryToolCtx = useContext(QueryToolContext);
const layoutDocker = useContext(LayoutDockerContext); const layoutDocker = useContext(LayoutDockerContext);
const lastCursorPos = React.useRef(); const lastCursorPos = React.useRef();
const lastSavedText = React.useRef('');
const markedLine = React.useRef(0);
const marker = React.useRef();
const pgAdmin = usePgAdmin(); const pgAdmin = usePgAdmin();
const preferencesStore = usePreferences(); 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})=>{ const highlightError = (cmObj, {errormsg: result, data})=>{
let errorLineNo = 0, let errorLineNo = 0,
startMarker = 0, startMarker = 0,
endMarker = 0, endMarker = 0,
selectedLineNo = 0, selectedLineNo = 1,
origQueryLen = cmObj.getValue().length; origQueryLen = cmObj.getValue().length;
removeHighlightError(cmObj); cmObj.removeErrorMark();
// In case of selection we need to find the actual line no // In case of selection we need to find the actual line no
if (cmObj.getSelection().length > 0) { if (cmObj.getSelection().length > 0) {
selectedLineNo = cmObj.getCursor(true).line; selectedLineNo = cmObj.getCurrentLineNo();
origQueryLen = cmObj.getLine(selectedLineNo).length; origQueryLen = cmObj.line(selectedLineNo).length;
} }
// Fetch the LINE string using regex from the result // 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 and character is null then no need to mark
if (line != null && char != null) { if (line != null && char != null) {
errorLineNo = (parseInt(line[1]) - 1) + selectedLineNo; errorLineNo = parseInt(line[1]) + selectedLineNo - 1;
let errorCharNo = (parseInt(char[1]) - 1); let errorCharNo = parseInt(char[1]) - 1;
/* If explain query has been run we need to /* If explain query has been run we need to
calculate the character number. calculate the character number.
@ -298,8 +109,8 @@ export default function Query() {
* have also added 1 per line for the "\n" character. * have also added 1 per line for the "\n" character.
*/ */
let prevLineChars = 0; let prevLineChars = 0;
for (let i = selectedLineNo > 0 ? selectedLineNo : 0; i < errorLineNo; i++) for (let i = selectedLineNo; i < errorLineNo; i++)
prevLineChars += cmObj.getLine(i).length + 1; prevLineChars += cmObj.getLine(i).length;
/* Marker starting point for the individual line is /* Marker starting point for the individual line is
* equal to error character index minus total no of * equal to error character index minus total no of
@ -316,18 +127,14 @@ export default function Query() {
endMarker = errorLine.length; endMarker = errorLine.length;
// Mark the error text // Mark the error text
marker.current = cmObj.markText({ cmObj.setErrorMark({
line: errorLineNo, line: errorLineNo,
ch: startMarker, pos: startMarker,
}, { }, {
line: errorLineNo, line: errorLineNo,
ch: endMarker, pos: endMarker,
}, {
className: 'sql-editor-mark',
}); });
markedLine.current = errorLineNo;
cmObj.addLineClass(errorLineNo, 'wrap', 'CodeMirror-activeline-background');
cmObj.focus(); cmObj.focus();
cmObj.setCursor(errorLineNo, endMarker); cmObj.setCursor(errorLineNo, endMarker);
} }
@ -364,7 +171,7 @@ export default function Query() {
if(result) { if(result) {
highlightError(editor.current, result); highlightError(editor.current, result);
} else { } else {
removeHighlightError(editor.current); editor.current.removeErrorMark();
} }
}); });
@ -381,7 +188,7 @@ export default function Query() {
editor.current.setValue(res.data); editor.current.setValue(res.data);
//Check the file content for Trojan Source //Check the file content for Trojan Source
checkTrojanSource(res.data); checkTrojanSource(res.data);
lastSavedText.current = res.data; editor.current.markClean();
eventBus.fireEvent(QUERY_TOOL_EVENTS.LOAD_FILE_DONE, fileName, true); eventBus.fireEvent(QUERY_TOOL_EVENTS.LOAD_FILE_DONE, fileName, true);
}).catch((err)=>{ }).catch((err)=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.LOAD_FILE_DONE, null, false); 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)=>{ eventBus.registerListener(QUERY_TOOL_EVENTS.SAVE_FILE, (fileName)=>{
let editorValue = editor.current.getValue();
queryToolCtx.api.post(url_for('sqleditor.save_file'), { queryToolCtx.api.post(url_for('sqleditor.save_file'), {
'file_name': decodeURI(fileName), 'file_name': decodeURI(fileName),
'file_content': editor.current.getValue(), 'file_content': editor.current.getValue(),
}).then(()=>{ }).then(()=>{
lastSavedText.current = editorValue; editor.current.markClean();
eventBus.fireEvent(QUERY_TOOL_EVENTS.SAVE_FILE_DONE, fileName, true); eventBus.fireEvent(QUERY_TOOL_EVENTS.SAVE_FILE_DONE, fileName, true);
pgAdmin.Browser.notifier.success(gettext('File saved successfully.')); pgAdmin.Browser.notifier.success(gettext('File saved successfully.'));
}).catch((err)=>{ }).catch((err)=>{
@ -426,16 +232,11 @@ export default function Query() {
key.shiftKey = false; key.shiftKey = false;
key.altKey = replace; key.altKey = replace;
} }
editor.current?.triggerOnKeyDown( editor.current?.fireDOMEvent(new KeyboardEvent('keydown', key));
new KeyboardEvent('keydown', key)
);
}); });
eventBus.registerListener(QUERY_TOOL_EVENTS.EDITOR_SET_SQL, (value, focus=true)=>{ eventBus.registerListener(QUERY_TOOL_EVENTS.EDITOR_SET_SQL, (value, focus=true)=>{
focus && editor.current?.focus(); focus && editor.current?.focus();
if(!queryToolCtx.params.is_query_tool){ editor.current?.setValue(value, !queryToolCtx.params.is_query_tool);
lastSavedText.current = value;
}
editor.current?.setValue(value);
if (value == '' && editor.current) { if (value == '' && editor.current) {
editor.current.state.autoCompleteList = []; editor.current.state.autoCompleteList = [];
} }
@ -500,7 +301,7 @@ export default function Query() {
useEffect(()=>{ useEffect(()=>{
const warnSaveTextClose = ()=>{ 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); eventBus.fireEvent(QUERY_TOOL_EVENTS.WARN_TXN_CLOSE);
return; return;
} }
@ -526,23 +327,20 @@ export default function Query() {
}, [queryToolCtx.preferences]); }, [queryToolCtx.preferences]);
useEffect(()=>{ 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);} (err)=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, err);}
); );
}, [queryToolCtx.params.trans_id]); }, [queryToolCtx.params.trans_id]);
const isDirty = ()=>(lastSavedText.current !== editor.current.getValue()); const cursorActivity = useCallback(_.debounce((cursor)=>{
lastCursorPos.current = cursor;
const cursorActivity = useCallback(_.debounce((cmObj)=>{ eventBus.fireEvent(QUERY_TOOL_EVENTS.CURSOR_ACTIVITY, [lastCursorPos.current.line, lastCursorPos.current.ch+1]);
const c = cmObj.getCursor();
lastCursorPos.current = c;
eventBus.fireEvent(QUERY_TOOL_EVENTS.CURSOR_ACTIVITY, [c.line+1, c.ch+1]);
}, 100), []); }, 100), []);
const change = useCallback(()=>{ 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){ if(queryToolCtx.preferences.sqleditor.view_edit_promotion_warning){
checkViewEditDataPromotion(); checkViewEditDataPromotion();
} else { } else {
@ -552,7 +350,7 @@ export default function Query() {
}, []); }, []);
const closePromotionWarning = (closeModal)=>{ const closePromotionWarning = (closeModal)=>{
if(isDirty()) { if(editor.current.isDirty()) {
editor.current.undo(); editor.current.undo();
closeModal?.(); closeModal?.();
} }
@ -567,7 +365,7 @@ export default function Query() {
promoteToQueryTool(); promoteToQueryTool();
let cursor = editor.current.getCursor(); let cursor = editor.current.getCursor();
editor.current.setValue(editor.current.getValue()); editor.current.setValue(editor.current.getValue());
editor.current.setCursor(cursor); editor.current.setCursor(cursor.line, cursor.ch);
editor.current.focus(); editor.current.focus();
let title = getTitle(pgAdmin, queryToolCtx.preferences.browser, null,null,queryToolCtx.params.server_name, queryToolCtx.params.dbname, queryToolCtx.params.user); let title = getTitle(pgAdmin, queryToolCtx.preferences.browser, null,null,queryToolCtx.params.server_name, queryToolCtx.params.dbname, queryToolCtx.params.user);
queryToolCtx.updateTitle(title); queryToolCtx.updateTitle(title);
@ -599,11 +397,9 @@ export default function Query() {
}} }}
value={''} value={''}
className={classes.sql} className={classes.sql}
events={{ onCursorActivity={cursorActivity}
'focus': cursorActivity, onChange={change}
'cursorActivity': cursorActivity,
'change': change,
}}
autocomplete={true} 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 ExplicitRoundedIcon from '@material-ui/icons/ExplicitRounded';
import { SaveDataIcon, CommitIcon, RollbackIcon, ViewDataIcon } from '../../../../../../static/js/components/ExternalIcon'; import { SaveDataIcon, CommitIcon, RollbackIcon, ViewDataIcon } from '../../../../../../static/js/components/ExternalIcon';
import { InputSwitch } from '../../../../../../static/js/components/FormComponents'; 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 { DefaultButton } from '../../../../../../static/js/components/Buttons';
import { useDelayedCaller } from '../../../../../../static/js/custom_hooks'; import { useDelayedCaller } from '../../../../../../static/js/custom_hooks';
import Loader from 'sources/components/Loader'; import Loader from 'sources/components/Loader';

View File

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

View File

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

View File

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

View File

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

View File

@ -235,12 +235,13 @@ class QueryToolLocators:
sql_editor_message = "//div[@id='id-messages'][contains(string(), '{}')]" 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 = \ 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']" btn_commit = "button[data-label='Commit']"

View File

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

View File

@ -168,20 +168,20 @@ describe('SchemaView', ()=>{
it('no changes', async ()=>{ it('no changes', async ()=>{
await user.click(ctrl.container.querySelector('button[data-test="SQL"]')); 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 ()=>{ it('data invalid', async ()=>{
await user.clear(ctrl.container.querySelector('[name="field2"]')); await user.clear(ctrl.container.querySelector('[name="field2"]'));
await user.type(ctrl.container.querySelector('[name="field2"]'), '2'); await user.type(ctrl.container.querySelector('[name="field2"]'), '2');
await user.click(ctrl.container.querySelector('button[data-test="SQL"]')); 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 ()=>{ it('valid data', async ()=>{
await simulateValidData(); await simulateValidData();
await user.click(ctrl.container.querySelector('button[data-test="SQL"]')); 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 React from 'react';
import {default as OrigCodeMirror} from 'bundled_codemirror';
import { withTheme } from '../fake_theme'; import { withTheme } from '../fake_theme';
import pgWindow from 'sources/window'; import pgWindow from 'sources/window';
import CodeMirror from 'sources/components/CodeMirror'; import CodeMirror from 'sources/components/ReactCodeMirror';
import { FindDialog } from '../../../pgadmin/static/js/components/CodeMirror'; import FindDialog from 'sources/components/ReactCodeMirror/FindDialog';
import CustomEditorView from 'sources/components/ReactCodeMirror/CustomEditorView';
import fakePgAdmin from '../fake_pgadmin'; 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', ()=>{ describe('CodeMirror', ()=>{
const ThemedCM = withTheme(CodeMirror); const ThemedCM = withTheme(CodeMirror);
let cmInstance, options={ let cmInstance, editor;
lineNumbers: true,
mode: 'text/x-pgsql',
},
cmObj = OrigCodeMirror.fromTextArea();
const cmRerender = (props)=>{ const cmRerender = (props)=>{
cmInstance.rerender( cmInstance.rerender(
<ThemedCM <ThemedCM
value={'Init text'} value={'Init text'}
options={options}
className="testClass" className="testClass"
currEditor={(obj) => {
editor = obj;
}}
{...props} {...props}
/> />
); );
}; };
beforeEach(()=>{ beforeEach(()=>{
pgWindow.pgAdmin = fakePgAdmin; pgWindow.pgAdmin = fakePgAdmin;
// jest.spyOn(OrigCodeMirror, 'fromTextArea').mockReturnValue(cmObj);
cmInstance = render( cmInstance = render(
<ThemedCM <ThemedCM
value={'Init text'} value={'Init text'}
options={options}
className="testClass" className="testClass"
currEditor={(obj) => {
editor = obj;
}}
/>); />);
}); });
@ -52,17 +65,23 @@ describe('CodeMirror', ()=>{
}); });
it('init', async ()=>{ it('init', async ()=>{
/* textarea ref passed to fromTextArea */ expect(CustomEditorView).toHaveBeenCalledTimes(1);
expect(OrigCodeMirror.fromTextArea).toHaveBeenCalledWith(cmInstance.container.querySelector('textarea'), expect.objectContaining(options)); expect(editor.setValue).toHaveBeenCalledWith('Init text');
await waitFor(() => expect(cmObj.setValue).toHaveBeenCalledWith('Init text'));
}); });
it('change value', ()=>{ it('change value', ()=>{
editor.state = {
doc: [],
};
editor.setValue.mockClear();
jest.spyOn(editor, 'getValue').mockReturnValue('Init text');
cmRerender({value: 'the new 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}); cmRerender({value: null});
expect(cmObj.setValue).toHaveBeenCalledWith(''); expect(editor.setValue).toHaveBeenCalledWith('');
}); });
@ -73,7 +92,7 @@ describe('CodeMirror', ()=>{
const ctrlMount = (props)=>{ const ctrlMount = (props)=>{
ctrl = render( ctrl = render(
<ThemedFindDialog <ThemedFindDialog
editor={cmObj} editor={editor}
show={true} show={true}
onClose={onClose} onClose={onClose}
{...props} {...props}
@ -84,29 +103,27 @@ describe('CodeMirror', ()=>{
it('init', ()=>{ it('init', ()=>{
ctrlMount({}); ctrlMount({});
cmObj.removeOverlay.mockClear(); CMSearch.SearchQuery.mockClear();
cmObj.addOverlay.mockClear();
const input = ctrl.container.querySelector('input'); const input = ctrl.container.querySelector('input');
fireEvent.change(input, { fireEvent.change(input, {
target: {value: '\n\r\tA'}, target: {value: '\n\r\tA'},
}); });
expect(cmObj.removeOverlay).toHaveBeenCalled(); expect(CMSearch.SearchQuery).toHaveBeenCalledWith(expect.objectContaining({
expect(cmObj.addOverlay).toHaveBeenCalled(); search: expect.stringContaining('A')
expect(cmObj.setSelection).toHaveBeenCalledWith(3, 14); }));
expect(cmObj.scrollIntoView).toHaveBeenCalled();
}); });
it('escape', ()=>{ it('escape', ()=>{
ctrlMount({}); ctrlMount({});
cmObj.removeOverlay.mockClear(); CMSearch.closeSearchPanel.mockClear();
fireEvent.keyDown(ctrl.container.querySelector('input'), { fireEvent.keyDown(ctrl.container.querySelector('input'), {
key: 'Escape', key: 'Escape',
}); });
expect(cmObj.removeOverlay).toHaveBeenCalled(); expect(CMSearch.closeSearchPanel).toHaveBeenCalled();
}); });
it('toggle match case', ()=>{ it('toggle match case', ()=>{
@ -132,7 +149,8 @@ describe('CodeMirror', ()=>{
it('replace', async ()=>{ it('replace', async ()=>{
ctrlMount({replace: true}); ctrlMount({replace: true});
cmObj.getSearchCursor().replace.mockClear(); CMSearch.SearchQuery.mockClear();
fireEvent.change(ctrl.container.querySelectorAll('input')[0], { fireEvent.change(ctrl.container.querySelectorAll('input')[0], {
target: {value: 'A'}, target: {value: 'A'},
}); });
@ -142,9 +160,13 @@ describe('CodeMirror', ()=>{
fireEvent.keyPress(ctrl.container.querySelectorAll('input')[1], { fireEvent.keyPress(ctrl.container.querySelectorAll('input')[1], {
key: 'Enter', shiftKey: true, code: 13, charCode: 13 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() {}; 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 jest.setTimeout(15000); // 1 second

View File

@ -357,7 +357,6 @@ module.exports = [{
// Specify entry points of application // Specify entry points of application
entry: { entry: {
'app.bundle': sourceDir + '/bundle/app.js', 'app.bundle': sourceDir + '/bundle/app.js',
codemirror: sourceDir + '/bundle/codemirror.js',
'security.pages': 'security.pages', 'security.pages': 'security.pages',
sqleditor: './pgadmin/tools/sqleditor/static/js/index.js', sqleditor: './pgadmin/tools/sqleditor/static/js/index.js',
schema_diff: './pgadmin/tools/schema_diff/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 // used by webpack while creating bundle
resolveAlias: { resolveAlias: {
'top': path.join(__dirname, './pgadmin/'), 'top': path.join(__dirname, './pgadmin/'),
'bundled_codemirror': path.join(__dirname, './pgadmin/static/bundle/codemirror'),
'bundled_browser': path.join(__dirname, './pgadmin/static/bundle/browser'), 'bundled_browser': path.join(__dirname, './pgadmin/static/bundle/browser'),
'sources': path.join(__dirname, './pgadmin/static/js/'), 'sources': path.join(__dirname, './pgadmin/static/js/'),
'translations': path.join(__dirname, './pgadmin/tools/templates/js/translations'), 'translations': path.join(__dirname, './pgadmin/tools/templates/js/translations'),

View File

@ -2354,6 +2354,103 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "@date-io/core@npm:1.x, @date-io/core@npm:^1.3.13, @date-io/core@npm:^1.3.6":
version: 1.3.13 version: 1.3.13
resolution: "@date-io/core@npm:1.3.13" resolution: "@date-io/core@npm:1.3.13"
@ -3086,6 +3183,31 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "@material-ui/core@npm:4.12.4":
version: 4.12.4 version: 4.12.4
resolution: "@material-ui/core@npm:4.12.4" resolution: "@material-ui/core@npm:4.12.4"
@ -6147,10 +6269,18 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"codemirror@npm:^5.59.2": "codemirror@npm:^6.0.1":
version: 5.65.13 version: 6.0.1
resolution: "codemirror@npm:5.65.13" resolution: "codemirror@npm:6.0.1"
checksum: 47060461edaebecd03b3fba4e73a30cdccc0c51ce3a3a05bafae3c9cafd682101383e94d77d54081eaf1ae18da5b74343e98343c637c52cea409956469039098 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 languageName: node
linkType: hard linkType: hard
@ -6504,6 +6634,13 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "cross-env@npm:^7.0.3":
version: 7.0.3 version: 7.0.3
resolution: "cross-env@npm:7.0.3" resolution: "cross-env@npm:7.0.3"
@ -14809,6 +14946,7 @@ __metadata:
"@babel/preset-env": ^7.10.2 "@babel/preset-env": ^7.10.2
"@babel/preset-react": ^7.12.13 "@babel/preset-react": ^7.12.13
"@babel/preset-typescript": ^7.22.5 "@babel/preset-typescript": ^7.22.5
"@codemirror/lang-sql": ^6.5.5
"@date-io/core": ^1.3.6 "@date-io/core": ^1.3.6
"@date-io/date-fns": 1.x "@date-io/date-fns": 1.x
"@emotion/core": ^10.0.14 "@emotion/core": ^10.0.14
@ -14852,7 +14990,7 @@ __metadata:
chartjs-plugin-zoom: ^2.0.1 chartjs-plugin-zoom: ^2.0.1
classnames: ^2.2.6 classnames: ^2.2.6
closest: ^0.0.1 closest: ^0.0.1
codemirror: ^5.59.2 codemirror: ^6.0.1
convert-units: ^2.3.4 convert-units: ^2.3.4
copy-webpack-plugin: ^11.0.0 copy-webpack-plugin: ^11.0.0
cross-env: ^7.0.3 cross-env: ^7.0.3
@ -15888,6 +16026,13 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "style-to-js@npm:1.1.9":
version: 1.1.9 version: 1.1.9
resolution: "style-to-js@npm:1.1.9" resolution: "style-to-js@npm:1.1.9"
@ -16916,6 +17061,13 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "w3c-xmlserializer@npm:^4.0.0":
version: 4.0.0 version: 4.0.0
resolution: "w3c-xmlserializer@npm:4.0.0" resolution: "w3c-xmlserializer@npm:4.0.0"