diff --git a/docs/en_US/images/preferences_sql_auto_completion.png b/docs/en_US/images/preferences_sql_auto_completion.png new file mode 100644 index 000000000..9c46a3c73 Binary files /dev/null and b/docs/en_US/images/preferences_sql_auto_completion.png differ diff --git a/docs/en_US/preferences.rst b/docs/en_US/preferences.rst index fc5af6602..e08733562 100644 --- a/docs/en_US/preferences.rst +++ b/docs/en_US/preferences.rst @@ -127,6 +127,13 @@ Please note: the default help paths include the *VERSION* placeholder; the $VERS Expand the *SQL Editor* node to access panels that allow you to specify your preferences for the SQL Editor tool. +.. image:: images/preferences_sql_auto_completion.png + :alt: Preferences dialog sqleditor auto completion option + +Use the fields on the *Auto Completion* panel to set the auto completion options. + +* When the *Keywords in uppercase* switch is set to *True* then keywords are shown in upper case. + .. image:: images/preferences_sql_csv_output.png :alt: Preferences dialog sqleditor csv output option diff --git a/docs/en_US/release_notes_3_1.rst b/docs/en_US/release_notes_3_1.rst index 8f8a759a0..4c2b16f83 100644 --- a/docs/en_US/release_notes_3_1.rst +++ b/docs/en_US/release_notes_3_1.rst @@ -11,6 +11,7 @@ Features ******** | `Feature #1447 `_ - Add support for SSH tunneled connections +| `Feature #2686 `_ - Add an option to auto-complete keywords in upper case | `Feature #3204 `_ - Add support for LISTEN/NOTIFY in the query tool | `Feature #3362 `_ - Function and procedure support for PG11 @@ -36,5 +37,5 @@ Bug fixes | `Bug #3342 `_ - Set SESSION_COOKIE_SAMESITE='Lax' per Flask recommendation to prevents sending cookies with CSRF-prone requests from external sites, such as submitting a form | `Bug #3353 `_ - Handle errors properly if they occur when renaming a database | `Bug #3374 `_ - Fix autocomplete -| `Bug #3392 `_ - Fix IPv6 support in the container build. +| `Bug #3392 `_ - Fix IPv6 support in the container build | `Bug #3409 `_ - Avoid an exception on GreenPlum when retrieving RE-SQL on a table \ No newline at end of file diff --git a/web/pgadmin/misc/templates/sqlautocomplete/sql/default/keywords.sql b/web/pgadmin/misc/templates/sqlautocomplete/sql/default/keywords.sql index cd571e170..9f762b8e6 100644 --- a/web/pgadmin/misc/templates/sqlautocomplete/sql/default/keywords.sql +++ b/web/pgadmin/misc/templates/sqlautocomplete/sql/default/keywords.sql @@ -1,2 +1,7 @@ {# SQL query for getting keywords #} +{% if upper_case %} SELECT upper(word) as word FROM pg_get_keywords() +{% else %} +SELECT word FROM pg_get_keywords() +{% endif %} + diff --git a/web/pgadmin/static/js/keyboard_shortcuts.js b/web/pgadmin/static/js/keyboard_shortcuts.js index 996a59192..1fe07e9b3 100644 --- a/web/pgadmin/static/js/keyboard_shortcuts.js +++ b/web/pgadmin/static/js/keyboard_shortcuts.js @@ -136,6 +136,7 @@ function keyboardShortcutsQueryTool( let downloadCsvKeys = keyboardShortcutConfig['download_csv']; let nextPanelKeys = keyboardShortcutConfig['move_next']; let previousPanelKeys = keyboardShortcutConfig['move_previous']; + let toggleCaseKeys = keyboardShortcutConfig['toggle_case']; if (this.validateShortcutKeys(executeKeys, event)) { this._stopEventPropagation(event); @@ -149,6 +150,9 @@ function keyboardShortcutsQueryTool( } else if (this.validateShortcutKeys(downloadCsvKeys, event)) { this._stopEventPropagation(event); queryToolActions.download(sqlEditorController); + } else if (this.validateShortcutKeys(toggleCaseKeys, event)) { + this._stopEventPropagation(event); + queryToolActions.toggleCaseOfSelectedText(sqlEditorController); } else if (( (this.isMac() && event.metaKey) || (!this.isMac() && event.ctrlKey) diff --git a/web/pgadmin/static/js/sqleditor/query_tool_actions.js b/web/pgadmin/static/js/sqleditor/query_tool_actions.js index 9e8e45055..411c6ed5f 100644 --- a/web/pgadmin/static/js/sqleditor/query_tool_actions.js +++ b/web/pgadmin/static/js/sqleditor/query_tool_actions.js @@ -132,6 +132,8 @@ let queryToolActions = { 'sqleditor', 'move_next'); let previousPanelPerf = window.top.pgAdmin.Browser.get_preference( 'sqleditor', 'move_previous'); + let toggleCasePerf = window.top.pgAdmin.Browser.get_preference( + 'sqleditor', 'toggle_case'); if(!executeQueryPref && sqlEditorController.handler.is_new_browser_tab) { executeQueryPref = window.opener.pgAdmin.Browser.get_preference( @@ -151,6 +153,9 @@ let queryToolActions = { ), previousPanelPerf = window.opener.pgAdmin.Browser.get_preference( 'sqleditor', 'move_previous' + ), + toggleCasePerf = window.opener.pgAdmin.Browser.get_preference( + 'sqleditor', 'toggle_case' ); } @@ -161,9 +166,23 @@ let queryToolActions = { 'download_csv': downloadCsvPref.value, 'move_next': nextPanelPerf.value, 'move_previous': previousPanelPerf.value, + 'toggle_case': toggleCasePerf.value, }; }, + + toggleCaseOfSelectedText: function (sqlEditorController) { + let codeMirrorObj = sqlEditorController.gridView.query_tool_obj; + let selectedText = codeMirrorObj.getSelection(); + + if (!selectedText) return; + + if (selectedText === selectedText.toUpperCase()) { + codeMirrorObj.replaceSelection(selectedText.toLowerCase()); + } else { + codeMirrorObj.replaceSelection(selectedText.toUpperCase()); + } + }, }; module.exports = queryToolActions; diff --git a/web/pgadmin/tools/sqleditor/utils/query_tool_preferences.py b/web/pgadmin/tools/sqleditor/utils/query_tool_preferences.py index 7c43d9b18..a5a7bcb83 100644 --- a/web/pgadmin/tools/sqleditor/utils/query_tool_preferences.py +++ b/web/pgadmin/tools/sqleditor/utils/query_tool_preferences.py @@ -535,6 +535,32 @@ def RegisterQueryToolPreferences(self): fields=accesskey_fields ) + self.preference.register( + 'keyboard_shortcuts', + 'toggle_case', + gettext('Toggle case of selected text'), + 'keyboardshortcut', + { + 'alt': False, + 'shift': True, + 'control': True, + 'key': { + 'key_code': 91, + 'char': 'u' + } + }, + category_label=gettext('Keyboard shortcuts'), + fields=shortcut_fields + ) + + self.preference.register( + 'auto_completion', 'keywords_in_uppercase', + gettext("Keywords in uppercase"), 'boolean', True, + category_label=gettext('Auto completion'), + help_str=gettext('If set to True, Keywords will be displayed ' + 'in upper case for auto completion.') + ) + def get_query_tool_keyboard_shortcuts(): @@ -564,6 +590,7 @@ def get_query_tool_keyboard_shortcuts(): explain_query = qt_perf.preference('explain_query').get() explain_analyze_query = qt_perf.preference('explain_analyze_query').get() find_options = qt_perf.preference('btn_find_options').get() + toggle_case = qt_perf.preference('toggle_case').get() return { 'keys': { @@ -587,7 +614,8 @@ def get_query_tool_keyboard_shortcuts(): 'explain_analyze_query': explain_analyze_query.get('key').get( 'char' ), - 'find_options': find_options.get('key').get('char') + 'find_options': find_options.get('key').get('char'), + 'toggle_case': toggle_case.get('key').get('char') }, 'shortcuts': { 'conn_status': conn_status, @@ -608,7 +636,8 @@ def get_query_tool_keyboard_shortcuts(): 'execute_query': execute_query, 'explain_query': explain_query, 'explain_analyze_query': explain_analyze_query, - 'find_options': find_options + 'find_options': find_options, + 'toggle_case': toggle_case }, } diff --git a/web/pgadmin/utils/sqlautocomplete/autocomplete.py b/web/pgadmin/utils/sqlautocomplete/autocomplete.py index af5514bdf..8a25c1324 100644 --- a/web/pgadmin/utils/sqlautocomplete/autocomplete.py +++ b/web/pgadmin/utils/sqlautocomplete/autocomplete.py @@ -26,6 +26,7 @@ from .function_metadata import FunctionMetadata from .parseutils import ( last_word, extract_tables, find_prev_keyword, parse_partial_identifier) from .prioritization import PrevalenceCounter +from pgadmin.utils.preferences import Preferences PY2 = sys.version_info[0] == 2 PY3 = sys.version_info[0] == 3 @@ -101,8 +102,17 @@ class SQLAutoComplete(object): for record in res['rows']: self.search_path.append(record['schema']) + pref = Preferences.module('sqleditor') + keywords_in_uppercase = \ + pref.preference('keywords_in_uppercase').get() + # Fetch the keywords query = render_template("/".join([self.sql_path, 'keywords.sql'])) + # If setting 'Keywords in uppercase' is set to True in + # Preferences then fetch the keywords in upper case. + if keywords_in_uppercase: + query = render_template( + "/".join([self.sql_path, 'keywords.sql']), upper_case=True) status, res = self.conn.execute_dict(query) if status: for record in res['rows']: diff --git a/web/regression/javascript/sqleditor/query_tool_actions_spec.js b/web/regression/javascript/sqleditor/query_tool_actions_spec.js index f3ac78153..61c18395d 100644 --- a/web/regression/javascript/sqleditor/query_tool_actions_spec.js +++ b/web/regression/javascript/sqleditor/query_tool_actions_spec.js @@ -11,7 +11,8 @@ import queryToolActions from 'sources/sqleditor/query_tool_actions'; describe('queryToolActions', () => { let sqlEditorController, getSelectionSpy, getValueSpy, - selectedQueryString, entireQueryString; + selectedQueryString, entireQueryString, + replaceSelectionSpy; describe('executeQuery', () => { describe('when the command is being run from the query tool', () => { @@ -437,9 +438,93 @@ describe('queryToolActions', () => { }); }); + describe('toggleCaseOfSelectedText', () => { + describe('when there is no query text', () => { + beforeEach(() => { + setUpSpies('', ''); + }); + it('does nothing', () => { + expect( + queryToolActions.toggleCaseOfSelectedText(sqlEditorController) + ).not.toBeDefined(); + }); + }); + + describe('when there is empty selection', () => { + beforeEach(() => { + setUpSpies('', 'a string\nddd\nsss'); + + sqlEditorController.gridView.query_tool_obj.getCursor = (isFrom) => { + return isFrom ? 3 : 3; + }; + }); + + it('does nothing', () => { + expect( + queryToolActions.toggleCaseOfSelectedText(sqlEditorController) + ).not.toBeDefined(); + }); + }); + + describe('when selected query is in lower case', () => { + beforeEach(() => { + setUpSpies('string', 'a string\nddd\nsss'); + }); + + it('toggle the selection and string should be in upper case', () => { + queryToolActions.toggleCaseOfSelectedText(sqlEditorController); + expect(replaceSelectionSpy + ).toHaveBeenCalledWith('STRING'); + }); + + it('(negative scenario toggle the selection and string should be in upper case', () => { + queryToolActions.toggleCaseOfSelectedText(sqlEditorController); + expect(replaceSelectionSpy + ).not.toHaveBeenCalledWith('string'); + }); + }); + + describe('when selected query is in upper case', () => { + beforeEach(() => { + setUpSpies('STRING', 'a string\nddd\nsss'); + }); + + it('toggle the selection and string should be in lower case', () => { + queryToolActions.toggleCaseOfSelectedText(sqlEditorController); + expect(replaceSelectionSpy + ).toHaveBeenCalledWith('string'); + }); + + it('(negative scenario toggle the selection and string should be in lower case', () => { + queryToolActions.toggleCaseOfSelectedText(sqlEditorController); + expect(replaceSelectionSpy + ).not.toHaveBeenCalledWith('STRING'); + }); + }); + + describe('when selected query is in mixed case', () => { + beforeEach(() => { + setUpSpies('sTRIng', 'a string\nddd\nsss'); + }); + + it('toggle the selection and string should be in upper case', () => { + queryToolActions.toggleCaseOfSelectedText(sqlEditorController); + expect(replaceSelectionSpy + ).toHaveBeenCalledWith('STRING'); + }); + + it('(negative scenario toggle the selection and string should be in upper case', () => { + queryToolActions.toggleCaseOfSelectedText(sqlEditorController); + expect(replaceSelectionSpy + ).not.toHaveBeenCalledWith('sTRIng'); + }); + }); + }); + function setUpSpies(selectedQueryString, entireQueryString) { getValueSpy = jasmine.createSpy('getValueSpy').and.returnValue(entireQueryString); getSelectionSpy = jasmine.createSpy('getSelectionSpy').and.returnValue(selectedQueryString); + replaceSelectionSpy = jasmine.createSpy('replaceSelectionSpy'); sqlEditorController = { gridView: { @@ -449,6 +534,7 @@ describe('queryToolActions', () => { toggleComment: jasmine.createSpy('toggleCommentSpy'), lineComment: jasmine.createSpy('lineCommentSpy'), uncomment: jasmine.createSpy('uncommentSpy'), + replaceSelection: replaceSelectionSpy, getCursor: (isFrom) => { return entireQueryString.indexOf(selectedQueryString) + (isFrom ? 0 : selectedQueryString.length); },