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/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/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..befef866be2 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/sections/Query.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/Query.jsx @@ -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), @@ -200,7 +198,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 +286,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; 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..47ba64d5d65 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/sections/StatusBar.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/StatusBar.jsx @@ -7,14 +7,15 @@ // This software is released under the PostgreSQL Licence // ////////////////////////////////////////////////////////////// -import React, { useEffect, useState, useContext } from 'react'; +import React, { useEffect, useState, useContext, useCallback } 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 { OS_EOL, QUERY_TOOL_EVENTS } from '../QueryToolConstants'; import { useStopwatch } from '../../../../../../static/js/custom_hooks'; import { QueryToolEventsContext } from '../QueryToolComponent'; import gettext from 'sources/gettext'; +import { PgMenu, PgMenuItem } 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() { const eventBus = useContext(QueryToolEventsContext); const [position, setPosition] = useState([1, 1]); const [lastTaskText, setLastTaskText] = useState(null); @@ -50,6 +51,35 @@ export function StatusBar() { }); const {seconds, minutes, hours, msec, start:startTimer, pause:pauseTimer, reset:resetTimer} = useStopwatch({}); + const [selectedEol, setSelectedEol] = useState(OS_EOL); + const [openMenuName, setOpenMenuName] = useState(null); + const prevMenuOpenIdRef = React.useRef(null); + const eolMenuRef = React.useRef(null); + + const toggleMenu = React.useCallback((e) => { + const name = e.currentTarget?.getAttribute('name'); + setOpenMenuName(() => { + return prevMenuOpenIdRef.current === name ? null : name; + }); + prevMenuOpenIdRef.current = null; + }, []); + + const onMenuClose = React.useCallback(() => { + prevMenuOpenIdRef.current = openMenuName; + setTimeout(() => { + prevMenuOpenIdRef.current = null; + }, 300); + setOpenMenuName(null); + }, [openMenuName]); + + const handleEndOfLineChange = useCallback((e)=>{ + const val = e.value; + const lineSep = val === 'crlf' ? '\r\n' : '\n'; + setSelectedEol(val); + eventBus.fireEvent(QUERY_TOOL_EVENTS.CHANGE_EOL, lineSep); + onMenuClose(); + }, []); + useEffect(()=>{ eventBus.registerListener(QUERY_TOOL_EVENTS.CURSOR_ACTIVITY, (newPos)=>{ setPosition(newPos||[1, 1]); @@ -109,7 +139,36 @@ export function StatusBar() { {gettext('Changes staged: %s', stagedText)} } - {gettext('Ln %s, Col %s', position[0], position[1])} + + + + + + {selectedEol.toUpperCase()} + + + + {gettext('LF')} + {gettext('CRLF')} + + + {gettext('Ln %s, Col %s', position[0], position[1])} + ); }