diff --git a/docs/en_US/images/preferences_sql_auto_completion.png b/docs/en_US/images/preferences_sql_auto_completion.png index d69cd16d9..d5618cb1b 100644 Binary files a/docs/en_US/images/preferences_sql_auto_completion.png 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 e985916cb..14cbf87ae 100644 --- a/docs/en_US/preferences.rst +++ b/docs/en_US/preferences.rst @@ -290,6 +290,9 @@ preferences for the Query Editor tool. Use the fields on the *Auto Completion* panel to set the auto completion options. +* When the *Autocomplete on key press* switch is set to *True* then autocomplete + will be available on key press along with CTRL/CMD + Space. If it is set to + *False* then autocomplete is only activated when CTRL/CMD + Space is pressed. * When the *Keywords in uppercase* switch is set to *True* then keywords are shown in upper case. diff --git a/docs/en_US/release_notes_6_12.rst b/docs/en_US/release_notes_6_12.rst index c515326e2..b2fe1d025 100644 --- a/docs/en_US/release_notes_6_12.rst +++ b/docs/en_US/release_notes_6_12.rst @@ -9,6 +9,7 @@ This release contains a number of bug fixes and new features since the release o New features ************ + | `Issue #4488 `_ - Added option to trigger autocomplete on key press in the query tool. | `Issue #7486 `_ - Added support for visualizing the graphs using Stacked Line, Bar, and Stacked Bar charts in the query tool. | `Issue #7487 `_ - Added support for visualise the graph using a Pie chart in the query tool. diff --git a/web/pgadmin/browser/templates/browser/css/node.css b/web/pgadmin/browser/templates/browser/css/node.css index c75627b0e..737df0894 100644 --- a/web/pgadmin/browser/templates/browser/css/node.css +++ b/web/pgadmin/browser/templates/browser/css/node.css @@ -16,3 +16,39 @@ background-size: 20px !important; background-position: center left; } + +.icon-at { + background-image: url('../../../../static/img/at.svg') !important; + background-repeat: no-repeat; + background-size: 15px !important; + align-content: center; + vertical-align: middle; + height: 15px; +} + +.icon-key { + background-image: url('../../../../static/img/key.svg') !important; + background-repeat: no-repeat; + background-size: 15px !important; + align-content: center; + vertical-align: middle; + height: 15px; +} + +.icon-join { + background-image: url('../../../../static/img/join.svg') !important; + background-repeat: no-repeat; + background-size: 20px !important; + align-content: center; + vertical-align: middle; + height: 15px; +} + +.icon-spinner { + background-image: url('../../../../static/img/spinner.svg') !important; + background-repeat: no-repeat; + background-size: 15px !important; + align-content: center; + vertical-align: middle; + height: 15px; +} diff --git a/web/pgadmin/static/img/at.svg b/web/pgadmin/static/img/at.svg new file mode 100644 index 000000000..967c3d08e --- /dev/null +++ b/web/pgadmin/static/img/at.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/web/pgadmin/static/img/join.svg b/web/pgadmin/static/img/join.svg new file mode 100644 index 000000000..30146a9ea --- /dev/null +++ b/web/pgadmin/static/img/join.svg @@ -0,0 +1 @@ +ex_join \ No newline at end of file diff --git a/web/pgadmin/static/img/key.svg b/web/pgadmin/static/img/key.svg new file mode 100644 index 000000000..0a2e7c831 --- /dev/null +++ b/web/pgadmin/static/img/key.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/web/pgadmin/static/img/spinner.svg b/web/pgadmin/static/img/spinner.svg new file mode 100644 index 000000000..4f9df2ff0 --- /dev/null +++ b/web/pgadmin/static/img/spinner.svg @@ -0,0 +1 @@ + diff --git a/web/pgadmin/static/js/components/CodeMirror.jsx b/web/pgadmin/static/js/components/CodeMirror.jsx index 7e3adff53..509e1ca5e 100644 --- a/web/pgadmin/static/js/components/CodeMirror.jsx +++ b/web/pgadmin/static/js/components/CodeMirror.jsx @@ -418,6 +418,17 @@ export default function CodeMirror({currEditor, name, value, options, events, re Object.keys(events||{}).forEach((eventName)=>{ editor.current.on(eventName, events[eventName]); }); + + // Register keyup event if autocomplete is true + let pref = pgWindow?.pgAdmin?.Browser?.get_preferences_for_module('sqleditor') || {}; + if (autocomplete && pref.autocomplete_on_key_press) { + editor.current.on('keyup', (cm, event)=>{ + if (!cm.state.completionActive && event.key != 'Enter' && event.key != 'Escape') { + OrigCodeMirror.commands.autocomplete(cm, null, {completeSingle: false}); + } + }); + } + editor.current.on('drop', handleDrop); editor.current.on('paste', handlePaste); initPreferences(); diff --git a/web/pgadmin/tools/sqleditor/__init__.py b/web/pgadmin/tools/sqleditor/__init__.py index 531798a68..7994f619d 100644 --- a/web/pgadmin/tools/sqleditor/__init__.py +++ b/web/pgadmin/tools/sqleditor/__init__.py @@ -61,6 +61,7 @@ MODULE_NAME = 'sqleditor' TRANSACTION_STATUS_CHECK_FAILED = gettext("Transaction status check failed.") _NODES_SQL = 'nodes.sql' sqleditor_close_session_lock = Lock() +auto_complete_objects = dict() class SqlEditorModule(PgAdminModule): @@ -559,6 +560,10 @@ def close(trans_id): trans_id: unique transaction id """ with sqleditor_close_session_lock: + # delete the SQLAutoComplete object + if trans_id in auto_complete_objects: + del auto_complete_objects[trans_id] + if 'gridData' not in session: return make_json_response(data={'status': True}) @@ -1746,10 +1751,13 @@ def auto_complete(trans_id): if status and conn is not None and \ trans_obj is not None and session_obj is not None: - # Create object of SQLAutoComplete class and pass connection object - auto_complete_obj = SQLAutoComplete( - sid=trans_obj.sid, did=trans_obj.did, conn=conn) + if trans_id not in auto_complete_objects: + # Create object of SQLAutoComplete class and pass connection object + auto_complete_objects[trans_id] = \ + SQLAutoComplete(sid=trans_obj.sid, did=trans_obj.did, + conn=conn) + auto_complete_obj = auto_complete_objects[trans_id] # Get the auto completion suggestions. res = auto_complete_obj.get_completions(full_sql, text_before_cursor) else: 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 8808ca711..1b7396ea3 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/sections/Query.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/Query.jsx @@ -27,6 +27,7 @@ const useStyles = makeStyles(()=>({ })); function registerAutocomplete(api, transId, onFailure) { + let timeoutId; OrigCodeMirror.registerHelper('hint', 'sql', function (editor) { var data = [], doc = editor.getDoc(), @@ -49,7 +50,6 @@ function registerAutocomplete(api, transId, onFailure) { */ hint_render: function (elt, data_arg, cur_arg) { var el = document.createElement('span'); - switch (cur_arg.type) { case 'database': el.className = 'sqleditor-hint pg-icon-' + cur_arg.type; @@ -58,10 +58,14 @@ function registerAutocomplete(api, transId, onFailure) { el.className = 'sqleditor-hint icon-type'; break; case 'keyword': - el.className = 'fa fa-key'; + el.className = 'sqleditor-hint icon-key'; break; case 'table alias': - el.className = 'fa fa-at'; + el.className = 'sqleditor-hint icon-at'; + break; + case 'join': + case 'fk join': + el.className = 'sqleditor-hint icon-join'; break; default: el.className = 'sqleditor-hint icon-' + cur_arg.type; @@ -87,73 +91,87 @@ function registerAutocomplete(api, transId, onFailure) { return { then: function (cb) { var self_local = this; - // Make ajax call to find the autocomplete data - api.post(self_local.url, JSON.stringify(self_local.data)) - .then((res) => { - var result = []; - _.each(res.data.data.result, function (obj, key) { - result.push({ - text: key, - type: obj.object_type, - render: self_local.hint_render, + /* + * Below logic find the start and end point + * to replace the selected auto complete suggestion. + */ + var token = self_local.editor.getTokenAt(cur), + start, end, search; + if (token.end > cur.ch) { + token.end = cur.ch; + token.string = token.string.slice(0, cur.ch - token.start); + } + + if (token.string.match(/^[.`\w@]\w*$/)) { + search = token.string; + start = token.start; + end = token.end; + } else { + start = end = cur.ch; + search = ''; + } + + /* + * Added 1 in the start position if search string + * started with "." or "`" else auto complete of code mirror + * will remove the "." when user select any suggestion. + */ + if (search.charAt(0) == '.' || search.charAt(0) == '``') + start += 1; + + cb({ + list: [{ + text: '', + render: (elt)=>{ + var el = document.createElement('span'); + el.className = 'sqleditor-hint icon-spinner'; + el.appendChild(document.createTextNode(gettext('Loading...'))); + elt.appendChild(el); + }, + }, {text: ''}], + from: { + line: self_local.current_line, + ch: start, + }, + to: { + line: self_local.current_line, + ch: end, + }, + }); + + timeoutId && clearTimeout(timeoutId); + timeoutId = setTimeout(()=> { + timeoutId = null; + // Make ajax call to find the autocomplete data + api.post(self_local.url, JSON.stringify(self_local.data)) + .then((res) => { + var result = []; + + _.each(res.data.data.result, function (obj, key) { + result.push({ + text: key, + type: obj.object_type, + render: self_local.hint_render, + }); }); + + cb({ + list: result, + from: { + line: self_local.current_line, + ch: start, + }, + to: { + line: self_local.current_line, + ch: end, + }, + }); + }) + .catch((err) => { + onFailure?.(err); }); - - // Sort function to sort the suggestion's alphabetically. - result.sort(function (a, b) { - var textA = a.text.toLowerCase(), - textB = b.text.toLowerCase(); - if (textA < textB) //sort string ascending - return -1; - if (textA > textB) - return 1; - return 0; //default return value (no sorting) - }); - - /* - * Below logic find the start and end point - * to replace the selected auto complete suggestion. - */ - var token = self_local.editor.getTokenAt(cur), - start, end, search; - if (token.end > cur.ch) { - token.end = cur.ch; - token.string = token.string.slice(0, cur.ch - token.start); - } - - if (token.string.match(/^[.`\w@]\w*$/)) { - search = token.string; - start = token.start; - end = token.end; - } else { - start = end = cur.ch; - search = ''; - } - - /* - * Added 1 in the start position if search string - * started with "." or "`" else auto complete of code mirror - * will remove the "." when user select any suggestion. - */ - if (search.charAt(0) == '.' || search.charAt(0) == '``') - start += 1; - - cb({ - list: result, - from: { - line: self_local.current_line, - ch: start, - }, - to: { - line: self_local.current_line, - ch: end, - }, - }); - }) - .catch((err) => { - onFailure?.(err); - }); + }, 300); }.bind(ctx), }; }); diff --git a/web/pgadmin/tools/sqleditor/utils/query_tool_preferences.py b/web/pgadmin/tools/sqleditor/utils/query_tool_preferences.py index 0dd4f23ca..d83720972 100644 --- a/web/pgadmin/tools/sqleditor/utils/query_tool_preferences.py +++ b/web/pgadmin/tools/sqleditor/utils/query_tool_preferences.py @@ -677,6 +677,16 @@ def register_query_tool_preferences(self): 'in upper case for auto completion.') ) + self.preference.register( + 'auto_completion', 'autocomplete_on_key_press', + gettext("Autocomplete on key press"), 'boolean', True, + category_label=gettext('Auto completion'), + help_str=gettext('If set to True, autocomplete will be available on ' + 'key press along with CTRL/CMD + Space. If set to ' + 'False, autocomplete is only activated when CTRL/CMD ' + '+ Space is pressed.') + ) + self.preference.register( 'keyboard_shortcuts', 'commit_transaction',