Added support for auto-detecting and setting the End-of-line character (LF/CRLF) in the query tool editor. #7393

This commit is contained in:
Rohit Bhati
2024-09-24 16:39:08 +05:30
committed by GitHub
parent fa64e8b38f
commit 4cb0f87dfd
10 changed files with 101 additions and 21 deletions
+2 -1
View File
@@ -91,8 +91,9 @@ export function usePgMenuGroup() {
const prevMenuOpenIdRef = useRef(null);
const toggleMenu = React.useCallback((e)=>{
const name = e.currentTarget?.getAttribute('name') || e.currentTarget?.name;
setOpenMenuName(()=>{
return prevMenuOpenIdRef.current == e.currentTarget?.name ? null : e.currentTarget?.name;
return prevMenuOpenIdRef.current == name ? null : name;
});
prevMenuOpenIdRef.current = null;
}, []);
@@ -9,7 +9,7 @@ import { errorMarkerEffect } from './extensions/errorMarker';
import { currentQueryHighlighterEffect } from './extensions/currentQueryHighlighter';
import { activeLineEffect, activeLineField } from './extensions/activeLineMarker';
import { clearBreakpoints, hasBreakpoint, toggleBreakpoint } from './extensions/breakpointGutter';
import { autoCompleteCompartment } from './extensions/extraStates';
import { autoCompleteCompartment, eol, eolCompartment } from './extensions/extraStates';
function getAutocompLoading({ bottom, left }, dom) {
@@ -30,11 +30,13 @@ export default class CustomEditorView extends EditorView {
this._cleanDoc = this.state.doc;
}
getValue(tillCursor=false) {
getValue(tillCursor=false, useLineSep=false) {
if(tillCursor) {
return this.state.sliceDoc(0, this.state.selection.main.head);
} else if (useLineSep) {
return this.state.doc.sliceString(0, this.state.doc.length, this.getEOL());
}
return this.state.doc.toString();
return this.state.sliceDoc();
}
/* Function to extract query based on position passed */
@@ -328,4 +330,14 @@ export default class CustomEditorView extends EditorView {
setQueryHighlightMark(from,to) {
this.dispatch({ effects: currentQueryHighlighterEffect.of({ from, to }) });
}
getEOL(){
return this.state.facet(eol);
}
setEOL(val){
this.dispatch({
effects: eolCompartment.reconfigure(eol.of(val))
});
}
}
@@ -51,7 +51,8 @@ import CustomEditorView from '../CustomEditorView';
import breakpointGutter, { breakpointEffect } from '../extensions/breakpointGutter';
import activeLineExtn from '../extensions/activeLineMarker';
import currentQueryHighlighterExtn from '../extensions/currentQueryHighlighter';
import { autoCompleteCompartment, indentNewLine } from '../extensions/extraStates';
import { autoCompleteCompartment, eolCompartment, indentNewLine, eol } from '../extensions/extraStates';
import { OS_EOL } from '../../../../../tools/sqleditor/static/js/components/QueryToolConstants';
const arrowRightHtml = ReactDOMServer.renderToString(<KeyboardArrowRightRoundedIcon style={{width: '16px'}} />);
const arrowDownHtml = ReactDOMServer.renderToString(<ExpandMoreRoundedIcon style={{width: '16px'}} />);
@@ -147,6 +148,10 @@ const defaultExtensions = [
return 0;
}),
autoCompleteCompartment.of([]),
EditorView.clipboardOutputFilter.of((text, state)=>{
const lineSep = state.facet(eol);
return state.doc.sliceString(0, text.length, lineSep);
})
];
export default function Editor({
@@ -174,6 +179,7 @@ export default function Editor({
useEffect(() => {
if (!checkIsMounted()) return;
const finalOptions = { ...defaultOptions, ...options };
const osEOL = OS_EOL === 'crlf' ? '\r\n' : '\n';
const finalExtns = [
(language == 'json') ? json() : sql({dialect: PgSQL}),
...defaultExtensions,
@@ -198,6 +204,7 @@ export default function Editor({
const state = EditorState.create({
extensions: [
...finalExtns,
eolCompartment.of([eol.of(osEOL)]),
shortcuts.current.of([]),
configurables.current.of([]),
editableConfig.current.of([
@@ -13,4 +13,10 @@ export const indentNewLine = Facet.define({
combine: values => values.length ? values[0] : true,
});
export const eol = Facet.define({
combine: values => values.length ? values[0] : '\n',
});
export const autoCompleteCompartment = new Compartment();
export const eolCompartment = new Compartment();
@@ -18,7 +18,7 @@ import { MainToolBar } from './sections/MainToolBar';
import { Messages } from './sections/Messages';
import getApiInstance, {callFetch, parseApiError} from '../../../../../static/js/api_instance';
import url_for from 'sources/url_for';
import { PANELS, QUERY_TOOL_EVENTS, CONNECTION_STATUS, MAX_QUERY_LENGTH } from './QueryToolConstants';
import { PANELS, QUERY_TOOL_EVENTS, CONNECTION_STATUS, MAX_QUERY_LENGTH, OS_EOL } from './QueryToolConstants';
import { useBeforeUnload, useInterval } from '../../../../../static/js/custom_hooks';
import { Box } from '@mui/material';
import { getDatabaseLabel, getTitle, setQueryToolDockerTitle } from '../sqleditor_title';
@@ -202,7 +202,8 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN
database_name: _.unescape(params.database_name) || getDatabaseLabel(selectedNodeInfo),
is_selected: true,
}],
editor_disabled:true
editor_disabled:true,
eol:OS_EOL
});
const [selectedText, setSelectedText] = useState('');
@@ -224,6 +225,14 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN
|| !qtState.is_visible) {
pollTime = -1;
}
const handleEndOfLineChange = useCallback((e)=>{
const val = e.value || e;
const lineSep = val === 'crlf' ? '\r\n' : '\n';
setQtStatePartial({ eol: val });
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.CHANGE_EOL, lineSep);
}, []);
useInterval(async ()=>{
try {
let {data: respData} = await fetchConnectionStatus(api, qtState.params.trans_id);
@@ -262,7 +271,7 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN
{
maximizable: true,
tabs: [
LayoutDocker.getPanel({id: PANELS.QUERY, title: gettext('Query'), content: <Query onTextSelect={(text) => setSelectedText(text)}/>}),
LayoutDocker.getPanel({id: PANELS.QUERY, title: gettext('Query'), content: <Query onTextSelect={(text) => setSelectedText(text)} handleEndOfLineChange={handleEndOfLineChange}/>}),
LayoutDocker.getPanel({id: PANELS.HISTORY, title: gettext('Query History'), content: <QueryHistory />,
cached: undefined}),
],
@@ -877,6 +886,7 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN
preferences: qtState.preferences,
mainContainerRef: containerRef,
editor_disabled: qtState.editor_disabled,
eol: qtState.eol,
toggleQueryTool: () => setQtStatePartial((prev)=>{
return {
...prev,
@@ -907,7 +917,7 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN
};
});
},
}), [qtState.params, qtState.preferences, containerRef.current, qtState.editor_disabled]);
}), [qtState.params, qtState.preferences, containerRef.current, qtState.editor_disabled, qtState.eol]);
const queryToolConnContextValue = React.useMemo(()=>({
connected: qtState.connected,
@@ -948,7 +958,7 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN
savedLayout={params.layout}
resetToTabPanel={PANELS.MESSAGES}
/>
<StatusBar />
<StatusBar eol={qtState.eol} handleEndOfLineChange={handleEndOfLineChange}/>
</Box>
</QueryToolEventsContext.Provider>
</QueryToolConnectionContext.Provider>
@@ -76,6 +76,7 @@ export const QUERY_TOOL_EVENTS = {
RESET_GRAPH_VISUALISER: 'RESET_GRAPH_VISUALISER',
GOTO_LAST_SCROLL: 'GOTO_LAST_SCROLL',
CHANGE_EOL: 'CHANGE_EOL'
};
export const CONNECTION_STATUS = {
@@ -106,4 +107,6 @@ export const PANELS = {
GRAPH_VISUALISER: 'id-graph-visualiser',
};
export const MAX_QUERY_LENGTH = 1000000;
export const MAX_QUERY_LENGTH = 1000000;
export const OS_EOL = navigator.platform === 'win32' ? 'crlf' : 'lf';
@@ -56,7 +56,7 @@ async function registerAutocomplete(editor, api, transId) {
});
}
export default function Query({onTextSelect}) {
export default function Query({onTextSelect, handleEndOfLineChange}) {
const editor = React.useRef();
const eventBus = useContext(QueryToolEventsContext);
const queryToolCtx = useContext(QueryToolContext);
@@ -65,7 +65,6 @@ export default function Query({onTextSelect}) {
const pgAdmin = usePgAdmin();
const preferencesStore = usePreferences();
const queryToolPref = queryToolCtx.preferences.sqleditor;
const highlightError = (cmObj, {errormsg: result, data}, executeCursor)=>{
let errorLineNo = 0,
startMarker = 0,
@@ -175,7 +174,6 @@ export default function Query({onTextSelect}) {
}
});
eventBus.registerListener(QUERY_TOOL_EVENTS.LOAD_FILE, (fileName, storage)=>{
queryToolCtx.api.post(url_for('sqleditor.load_file'), {
'file_name': decodeURI(fileName),
@@ -191,6 +189,8 @@ export default function Query({onTextSelect}) {
checkTrojanSource(res.data);
editor.current.markClean();
eventBus.fireEvent(QUERY_TOOL_EVENTS.LOAD_FILE_DONE, fileName, true);
const lineSep = res.data.includes('\r\n') ? 'crlf' : 'lf';
handleEndOfLineChange(lineSep);
}).catch((err)=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.LOAD_FILE_DONE, null, false);
pgAdmin.Browser.notifier.error(parseApiError(err));
@@ -200,7 +200,7 @@ export default function Query({onTextSelect}) {
eventBus.registerListener(QUERY_TOOL_EVENTS.SAVE_FILE, (fileName)=>{
queryToolCtx.api.post(url_for('sqleditor.save_file'), {
'file_name': decodeURI(fileName),
'file_content': editor.current.getValue(),
'file_content': editor.current.getValue(false, true),
}).then(()=>{
editor.current.markClean();
eventBus.fireEvent(QUERY_TOOL_EVENTS.SAVE_FILE_DONE, fileName, true);
@@ -288,6 +288,12 @@ export default function Query({onTextSelect}) {
editor.current.setValue(formattedSql);
}
});
eventBus.registerListener(QUERY_TOOL_EVENTS.CHANGE_EOL, (lineSep)=>{
editor.current?.setEOL(lineSep);
eventBus.fireEvent(QUERY_TOOL_EVENTS.QUERY_CHANGED, true);
});
eventBus.registerListener(QUERY_TOOL_EVENTS.EDITOR_TOGGLE_CASE, ()=>{
let selectedText = editor.current?.getSelection();
if (!selectedText) return;
@@ -518,4 +524,5 @@ export default function Query({onTextSelect}) {
Query.propTypes = {
onTextSelect: PropTypes.func,
handleEndOfLineChange: PropTypes.func
};
@@ -9,12 +9,13 @@
//////////////////////////////////////////////////////////////
import React, { useEffect, useState, useContext } from 'react';
import { styled } from '@mui/material/styles';
import { Box } from '@mui/material';
import { Box, Tooltip } from '@mui/material';
import _ from 'lodash';
import { QUERY_TOOL_EVENTS } from '../QueryToolConstants';
import { useStopwatch } from '../../../../../../static/js/custom_hooks';
import { QueryToolEventsContext } from '../QueryToolComponent';
import gettext from 'sources/gettext';
import { PgMenu, PgMenuItem, usePgMenuGroup } from '../../../../../../static/js/components/Menu';
const StyledBox = styled(Box)(({theme}) => ({
@@ -26,17 +27,17 @@ const StyledBox = styled(Box)(({theme}) => ({
userSelect: 'text',
'& .StatusBar-padding': {
padding: '2px 12px',
'& .StatusBar-mlAuto': {
'&.StatusBar-mlAuto': {
marginLeft: 'auto',
},
'& .StatusBar-divider': {
'&.StatusBar-divider': {
...theme.mixins.panelBorder.right,
},
},
}));
export function StatusBar() {
export function StatusBar({eol, handleEndOfLineChange}) {
const eventBus = useContext(QueryToolEventsContext);
const [position, setPosition] = useState([1, 1]);
const [lastTaskText, setLastTaskText] = useState(null);
@@ -49,6 +50,8 @@ export function StatusBar() {
deleted: 0,
});
const {seconds, minutes, hours, msec, start:startTimer, pause:pauseTimer, reset:resetTimer} = useStopwatch({});
const eolMenuRef = React.useRef(null);
const {openMenuName, toggleMenu, onMenuClose} = usePgMenuGroup();
useEffect(()=>{
eventBus.registerListener(QUERY_TOOL_EVENTS.CURSOR_ACTIVITY, (newPos)=>{
@@ -109,7 +112,36 @@ export function StatusBar() {
<span>{gettext('Changes staged: %s', stagedText)}</span>
</Box>
}
<Box className='StatusBar-padding StatusBar-mlAuto'>{gettext('Ln %s, Col %s', position[0], position[1])}</Box>
<Box className='StatusBar-padding StatusBar-mlAuto' style={{display:'flex'}}>
<Box className="StatusBar-padding StatusBar-divider">
<Tooltip title="Select EOL Sequence" disableInteractive enterDelay={2500}>
<span
onClick={toggleMenu}
ref={eolMenuRef}
name="menu-eoloptions"
style={{
cursor: 'pointer',
display: 'inline-flex',
alignItems: 'center',
fontSize: 'inherit',
}}
>
{eol.toUpperCase()}
</span>
</Tooltip>
<PgMenu
anchorRef={eolMenuRef}
open={openMenuName=='menu-eoloptions'}
onClose={onMenuClose}
label={gettext('EOL Options Menu')}
>
<PgMenuItem hasCheck value="lf" checked={eol === 'lf'} onClick={handleEndOfLineChange}>{gettext('LF')}</PgMenuItem>
<PgMenuItem hasCheck value="crlf" checked={eol === 'crlf'} onClick={handleEndOfLineChange}>{gettext('CRLF')}</PgMenuItem>
</PgMenu>
</Box>
<Box className='StatusBar-padding'>{gettext('Ln %s, Col %s', position[0], position[1])}</Box>
</Box>
</StyledBox>
);
}