diff --git a/docs/en_US/images/query_status_bar.png b/docs/en_US/images/query_status_bar.png
index e885920ba97..fdc2d610023 100644
Binary files a/docs/en_US/images/query_status_bar.png and b/docs/en_US/images/query_status_bar.png differ
diff --git a/docs/en_US/query_tool_toolbar.rst b/docs/en_US/query_tool_toolbar.rst
index 42db9874a74..ccafd633462 100644
--- a/docs/en_US/query_tool_toolbar.rst
+++ b/docs/en_US/query_tool_toolbar.rst
@@ -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
diff --git a/web/pgadmin/static/js/components/Menu.jsx b/web/pgadmin/static/js/components/Menu.jsx
index 49536289634..12d0f4db152 100644
--- a/web/pgadmin/static/js/components/Menu.jsx
+++ b/web/pgadmin/static/js/components/Menu.jsx
@@ -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;
}, []);
diff --git a/web/pgadmin/static/js/components/ReactCodeMirror/CustomEditorView.js b/web/pgadmin/static/js/components/ReactCodeMirror/CustomEditorView.js
index 6801b44a77c..b48f77a92d3 100644
--- a/web/pgadmin/static/js/components/ReactCodeMirror/CustomEditorView.js
+++ b/web/pgadmin/static/js/components/ReactCodeMirror/CustomEditorView.js
@@ -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))
+ });
+ }
}
diff --git a/web/pgadmin/static/js/components/ReactCodeMirror/components/Editor.jsx b/web/pgadmin/static/js/components/ReactCodeMirror/components/Editor.jsx
index f7c22f74484..1abd7be430b 100644
--- a/web/pgadmin/static/js/components/ReactCodeMirror/components/Editor.jsx
+++ b/web/pgadmin/static/js/components/ReactCodeMirror/components/Editor.jsx
@@ -48,7 +48,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();
const arrowDownHtml = ReactDOMServer.renderToString();
@@ -144,6 +145,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({
@@ -167,6 +172,7 @@ export default function Editor({
useEffect(() => {
const finalOptions = { ...defaultOptions, ...options };
+ const osEOL = OS_EOL === 'crlf' ? '\r\n' : '\n';
const finalExtns = [
(language == 'json') ? json() : sql({dialect: PgSQL}),
...defaultExtensions,
@@ -191,6 +197,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([
diff --git a/web/pgadmin/static/js/components/ReactCodeMirror/extensions/extraStates.js b/web/pgadmin/static/js/components/ReactCodeMirror/extensions/extraStates.js
index 5e4c7b433ea..0970c4cddd4 100644
--- a/web/pgadmin/static/js/components/ReactCodeMirror/extensions/extraStates.js
+++ b/web/pgadmin/static/js/components/ReactCodeMirror/extensions/extraStates.js
@@ -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();
+
diff --git a/web/pgadmin/tools/sqleditor/static/js/components/QueryToolComponent.jsx b/web/pgadmin/tools/sqleditor/static/js/components/QueryToolComponent.jsx
index 321b6e18d78..657a1f3663c 100644
--- a/web/pgadmin/tools/sqleditor/static/js/components/QueryToolComponent.jsx
+++ b/web/pgadmin/tools/sqleditor/static/js/components/QueryToolComponent.jsx
@@ -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('');
@@ -262,7 +263,7 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN
{
maximizable: true,
tabs: [
- LayoutDocker.getPanel({id: PANELS.QUERY, title: gettext('Query'), content: setSelectedText(text)}/>}),
+ LayoutDocker.getPanel({id: PANELS.QUERY, title: gettext('Query'), content: setSelectedText(text)} handleEndOfLineChange={handleEndOfLineChange}/>}),
LayoutDocker.getPanel({id: PANELS.HISTORY, title: gettext('Query History'), content: ,
cached: undefined}),
],
@@ -308,6 +309,13 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN
},
};
+ 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);
+ }, []);
+
const getSQLScript = () => {
// Fetch the SQL for Scripts (eg: CREATE/UPDATE/DELETE/SELECT)
// Call AJAX only if script type URL is present
@@ -869,6 +877,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,
@@ -899,7 +908,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,
@@ -940,7 +949,7 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN
savedLayout={params.layout}
resetToTabPanel={PANELS.MESSAGES}
/>
-
+
diff --git a/web/pgadmin/tools/sqleditor/static/js/components/QueryToolConstants.js b/web/pgadmin/tools/sqleditor/static/js/components/QueryToolConstants.js
index a1c3825679e..ddbeb91ec96 100644
--- a/web/pgadmin/tools/sqleditor/static/js/components/QueryToolConstants.js
+++ b/web/pgadmin/tools/sqleditor/static/js/components/QueryToolConstants.js
@@ -75,6 +75,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 = {
@@ -105,4 +106,6 @@ export const PANELS = {
GRAPH_VISUALISER: 'id-graph-visualiser',
};
-export const MAX_QUERY_LENGTH = 1000000;
\ No newline at end of file
+export const MAX_QUERY_LENGTH = 1000000;
+
+export const OS_EOL = navigator.platform === 'win32' ? 'crlf' : 'lf';
\ No newline at end of file
diff --git a/web/pgadmin/tools/sqleditor/static/js/components/sections/Query.jsx b/web/pgadmin/tools/sqleditor/static/js/components/sections/Query.jsx
index 178c011ad99..c619a47887d 100644
--- a/web/pgadmin/tools/sqleditor/static/js/components/sections/Query.jsx
+++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/Query.jsx
@@ -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
};
diff --git a/web/pgadmin/tools/sqleditor/static/js/components/sections/StatusBar.jsx b/web/pgadmin/tools/sqleditor/static/js/components/sections/StatusBar.jsx
index c4dbf06cc7b..79a7324a3d0 100644
--- a/web/pgadmin/tools/sqleditor/static/js/components/sections/StatusBar.jsx
+++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/StatusBar.jsx
@@ -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() {
{gettext('Changes staged: %s', stagedText)}
}
- {gettext('Ln %s, Col %s', position[0], position[1])}
+
+
+
+
+
+ {eol.toUpperCase()}
+
+
+
+ {gettext('LF')}
+ {gettext('CRLF')}
+
+
+ {gettext('Ln %s, Col %s', position[0], position[1])}
+
);
}