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

pull/7974/head
Rohit Bhati 2024-09-24 16:39:08 +05:30 committed by GitHub
parent fa64e8b38f
commit 4cb0f87dfd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 101 additions and 21 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -226,6 +226,8 @@ The status bar shows the following information:
* **Total rows**: The total number of rows returned by the query.
* **Query complete**: The time is taken by the query to complete.
* **Rows selected**: The number of rows selected in the data output panel.
* **Changes staged**: This information showed the number of rows added, deleted, and updated.
* **Changes staged**: This information shows the number of rows added, deleted, and updated.
* **LF/CRLF**: It shows the end of line sequence to be used for the editor. When opening an empty editor, it will be decided based on OS.
And when opening an existing file, it will be based on file end of lines. One can change the EOL by clicking on any of the options.
* **Ln**: In the Query tab, it is the line number at which the cursor is positioned.
* **Col**: In the Query tab, it is the column number at which the cursor is positioned

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;
}, []);

View File

@ -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))
});
}
}

View File

@ -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([

View File

@ -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();

View File

@ -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>

View File

@ -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';

View File

@ -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
};

View File

@ -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>
);
}