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
53 changed files with 1565 additions and 1611 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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