Added support for auto-detecting and setting the End-of-line character (LF/CRLF) in the query tool editor. #7393
parent
fa64e8b38f
commit
4cb0f87dfd
Binary file not shown.
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 46 KiB |
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue