From ee0b4f8103a49946dc308c9c56dd7226528a4966 Mon Sep 17 00:00:00 2001 From: Martyn Gigg <martyn.gigg@gmail.com> Date: Tue, 16 Jan 2018 11:39:44 +0000 Subject: [PATCH] Implement autocompletion and calltips in the editor. Refs #21251 --- .../workbench/workbench/plugins/editor.py | 2 +- .../mantidqt/widgets/codeeditor/execution.py | 94 +++++++++++++++++-- .../widgets/codeeditor/inputsplitter.py | 1 - .../widgets/codeeditor/interpreter.py | 3 + .../widgets/codeeditor/test/test_execution.py | 26 +++-- .../mantidqt/widgets/src/_widgetscore.sip | 11 +++ .../inc/MantidQtWidgets/Common/ScriptEditor.h | 6 +- qt/widgets/common/src/ScriptEditor.cpp | 7 +- 8 files changed, 120 insertions(+), 30 deletions(-) diff --git a/qt/applications/workbench/workbench/plugins/editor.py b/qt/applications/workbench/workbench/plugins/editor.py index bcadfd8b025..c8ec86820d5 100644 --- a/qt/applications/workbench/workbench/plugins/editor.py +++ b/qt/applications/workbench/workbench/plugins/editor.py @@ -58,7 +58,7 @@ class MultiFileEditor(PluginWidget): shortcut="Ctrl+Return", shortcut_context=Qt.ApplicationShortcut) self.abort_action = create_action(self, "Abort", - on_triggered=self.editors.abort_current) + on_triggered=self.editors.abort_current) self.editor_actions = [self.run_action, self.abort_action] diff --git a/qt/python/mantidqt/widgets/codeeditor/execution.py b/qt/python/mantidqt/widgets/codeeditor/execution.py index 81792f56c47..7f5f78bc7ab 100644 --- a/qt/python/mantidqt/widgets/codeeditor/execution.py +++ b/qt/python/mantidqt/widgets/codeeditor/execution.py @@ -18,15 +18,85 @@ from __future__ import (absolute_import, unicode_literals) # std imports import ctypes +import inspect import time # 3rdparty imports from qtpy.QtCore import QObject, Signal +from six import PY2, iteritems # local imports from mantidqt.widgets.codeeditor.inputsplitter import InputSplitter from mantidqt.utils.async import AsyncTask +if PY2: + from inspect import getargspec as getfullargspec +else: + from inspect import getfullargspec + + +def get_function_spec(func): + """Get the python function signature for the given function object. First + the args are inspected followed by varargs, which are set by some modules, + e.g. mantid.simpleapi algorithm functions + + :param func: A Python function object + :returns: A string containing the function specification + : + """ + try: + argspec = getfullargspec(func) + except TypeError: + return '' + # mantid algorithm functions have varargs set not args + args = argspec[0] + if args: + # For methods strip the self argument + if hasattr(func, 'im_func'): + args = args[1:] + defs = argspec[3] + elif argspec[1] is not None: + # Get from varargs/keywords + arg_str = argspec[1].strip().lstrip('\b') + defs = [] + # Keyword args + kwargs = argspec[2] + if kwargs is not None: + kwargs = kwargs.strip().lstrip('\b\b') + if kwargs == 'kwargs': + kwargs = '**' + kwargs + '=None' + arg_str += ',%s' % kwargs + # Any default argument appears in the string + # on the rhs of an equal + for arg in arg_str.split(','): + arg = arg.strip() + if '=' in arg: + arg_token = arg.split('=') + args.append(arg_token[0]) + defs.append(arg_token[1]) + else: + args.append(arg) + if len(defs) == 0: + defs = None + else: + return '' + + if defs is None: + calltip = ','.join(args) + calltip = '(' + calltip + ')' + else: + # The defaults list contains the default values for the last n arguments + diff = len(args) - len(defs) + calltip = '' + for index in range(len(args) - 1, -1, -1): + def_index = index - diff + if def_index >= 0: + calltip = '[' + args[index] + '],' + calltip + else: + calltip = args[index] + "," + calltip + calltip = '(' + calltip.rstrip(',') + ')' + return calltip + class PythonCodeExecution(QObject): """Provides the ability to execute arbitrary @@ -41,7 +111,7 @@ class PythonCodeExecution(QObject): """Initialize the object""" super(PythonCodeExecution, self).__init__() - self._globals_ns, self._locals_ns = None, None + self._globals_ns = None self._task = None self.reset_context() @@ -50,10 +120,6 @@ class PythonCodeExecution(QObject): def globals_ns(self): return self._globals_ns - @property - def locals_ns(self): - return self._locals_ns - def abort(self): """Cancel an asynchronous execution""" # Implementation is based on @@ -97,11 +163,25 @@ class PythonCodeExecution(QObject): sig_progress.emit(block.lineno) # compile so we can set the filename code_obj = compile(block.code_str, filename, mode='exec') - exec(code_obj, self.globals_ns, self.locals_ns) + exec(code_obj, self.globals_ns, self.globals_ns) + + def generate_calltips(self): + """ + Return a list of calltips for the current global scope. This is currently + very basic and only inspects the available functions and builtins at the current scope. + + :return: A list of strings giving calltips for each global callable + """ + calltips = [] + for name, attr in iteritems(self._globals_ns): + if inspect.isfunction(attr) or inspect.isbuiltin(attr): + calltips.append(name + get_function_spec(attr)) + + return calltips def reset_context(self): # create new context for execution - self._globals_ns, self._locals_ns = {}, {} + self._globals_ns, self._namespace = {}, {} # --------------------- Callbacks ------------------------------- def _on_success(self, task_result): diff --git a/qt/python/mantidqt/widgets/codeeditor/inputsplitter.py b/qt/python/mantidqt/widgets/codeeditor/inputsplitter.py index f38b3913cba..738a33a264c 100644 --- a/qt/python/mantidqt/widgets/codeeditor/inputsplitter.py +++ b/qt/python/mantidqt/widgets/codeeditor/inputsplitter.py @@ -115,4 +115,3 @@ class InputSplitter(IPyInputSplitter): # General fallback - accept more code return True - diff --git a/qt/python/mantidqt/widgets/codeeditor/interpreter.py b/qt/python/mantidqt/widgets/codeeditor/interpreter.py index 037c8fad6e7..40aafa85b2a 100644 --- a/qt/python/mantidqt/widgets/codeeditor/interpreter.py +++ b/qt/python/mantidqt/widgets/codeeditor/interpreter.py @@ -176,6 +176,8 @@ class PythonFileInterpreter(QWidget): else: editor.setModified(True) + editor.enableAutoCompletion(CodeEditor.AcsAll) + class PythonFileInterpreterPresenter(QObject): """Presenter part of MVP to control actions on the editor""" @@ -232,6 +234,7 @@ class PythonFileInterpreterPresenter(QObject): return code_str, line_from def _on_exec_success(self, task_result): + self.view.editor.updateCompletionAPI(self.model.generate_calltips()) self._finish(success=True, elapsed_time=task_result.elapsed_time) def _on_exec_error(self, task_error): diff --git a/qt/python/mantidqt/widgets/codeeditor/test/test_execution.py b/qt/python/mantidqt/widgets/codeeditor/test/test_execution.py index 5e08558d5c0..f8effdbb870 100644 --- a/qt/python/mantidqt/widgets/codeeditor/test/test_execution.py +++ b/qt/python/mantidqt/widgets/codeeditor/test/test_execution.py @@ -56,28 +56,24 @@ class PythonCodeExecutionTest(unittest.TestCase): def test_default_construction_yields_empty_context(self): executor = PythonCodeExecution() self.assertEqual(0, len(executor.globals_ns)) - self.assertEqual(0, len(executor.locals_ns)) - def test_reset_context_clears_globals_and_locals(self): + def test_reset_context_clears_context(self): executor = PythonCodeExecution() + globals_len = len(executor.globals_ns) executor.execute("x = 1") - self.assertEqual(1, len(executor.globals_ns)) - self.assertEqual(1, len(executor.locals_ns)) + self.assertTrue(globals_len + 1, len(executor.globals_ns)) executor.reset_context() self.assertEqual(0, len(executor.globals_ns)) - self.assertEqual(0, len(executor.locals_ns)) # --------------------------------------------------------------------------- # Successful execution tests # --------------------------------------------------------------------------- - def test_execute_places_output_in_locals_mapping_if_different_to_globals(self): + def test_execute_places_output_in_globals(self): code = "_local=100" - user_globals, user_locals = self._verify_serial_execution_successful(code) - self.assertEquals(100, user_locals['_local']) - self.assertTrue('_local' not in user_globals) - user_globals, user_locals = self._verify_async_execution_successful(code) - self.assertEquals(100, user_locals['_local']) - self.assertTrue('_local' not in user_globals) + user_globals = self._verify_serial_execution_successful(code) + self.assertEquals(100, user_globals['_local']) + user_globals = self._verify_async_execution_successful(code) + self.assertEquals(100, user_globals['_local']) def test_execute_async_calls_success_signal_on_completion(self): code = "x=1+2" @@ -186,7 +182,7 @@ squared = sum*sum else: self.fail("No callback was called!") - context = executor.locals_ns + context = executor.globals_ns self.assertEqual(20, context['sum']) self.assertEqual(20*20, context['squared']) self.assertEqual(1, context['x']) @@ -208,13 +204,13 @@ squared = sum*sum def _verify_serial_execution_successful(self, code): executor = PythonCodeExecution() executor.execute(code) - return executor.globals_ns, executor.locals_ns + return executor.globals_ns def _verify_async_execution_successful(self, code): executor = PythonCodeExecution() task = executor.execute_async(code) task.join() - return executor.globals_ns, executor.locals_ns + return executor.globals_ns def _verify_failed_serial_execute(self, expected_exc_type, code): executor = PythonCodeExecution() diff --git a/qt/python/mantidqt/widgets/src/_widgetscore.sip b/qt/python/mantidqt/widgets/src/_widgetscore.sip index be14405d4f4..4f930b88b72 100644 --- a/qt/python/mantidqt/widgets/src/_widgetscore.sip +++ b/qt/python/mantidqt/widgets/src/_widgetscore.sip @@ -67,6 +67,14 @@ class ScriptEditor : QWidget { #include "MantidQtWidgets/Common/ScriptEditor.h" %End +public: + enum AutoCompletionSource { + AcsNone, + AcsAll, + AcsDocument, + AcsAPIs + }; + public: ScriptEditor(const QString & language, QWidget *parent /TransferThis/ = 0) throw(std::invalid_argument); @@ -85,6 +93,8 @@ public: QString selectedText() const; QString text() const; + void enableAutoCompletion(AutoCompletionSource source); + void disableAutoCompletion(); void setCaretLineBackgroundColor (const QColor & col); void setCaretLineVisible(bool enable); void setIndentationsUseTabs(bool tabs); @@ -95,6 +105,7 @@ public: void setSelection(int lineFrom, int indexFrom, int lineTo, int indexTo); void setTabWidth(int width); void setText(const QString &text); + void updateCompletionAPI(const QStringList & completions); public slots: void updateProgressMarker(int lineno, bool error); diff --git a/qt/widgets/common/inc/MantidQtWidgets/Common/ScriptEditor.h b/qt/widgets/common/inc/MantidQtWidgets/Common/ScriptEditor.h index 54f39ef8f18..48714389ecc 100644 --- a/qt/widgets/common/inc/MantidQtWidgets/Common/ScriptEditor.h +++ b/qt/widgets/common/inc/MantidQtWidgets/Common/ScriptEditor.h @@ -87,8 +87,10 @@ public: void setLexer(QsciLexer *) override; // Make the object resize to margin to fit the contents void setAutoMarginResize(); - /// Enable the auto complete - void enableAutoCompletion(); + /// Enable the auto complete. Default is for backwards compatability + /// with existing code + void + enableAutoCompletion(AutoCompletionSource source = QsciScintilla::AcsAPIs); /// Disable the auto complete void disableAutoCompletion(); diff --git a/qt/widgets/common/src/ScriptEditor.cpp b/qt/widgets/common/src/ScriptEditor.cpp index 39d16e8317e..9a5ab89d7f7 100644 --- a/qt/widgets/common/src/ScriptEditor.cpp +++ b/qt/widgets/common/src/ScriptEditor.cpp @@ -177,10 +177,10 @@ void ScriptEditor::setAutoMarginResize() { /** * Enable the auto complete */ -void ScriptEditor::enableAutoCompletion() { - setAutoCompletionSource(QsciScintilla::AcsAPIs); - setCallTipsVisible(QsciScintilla::CallTipsNoAutoCompletionContext); +void ScriptEditor::enableAutoCompletion(AutoCompletionSource source) { + setAutoCompletionSource(source); setAutoCompletionThreshold(2); + setCallTipsStyle(QsciScintilla::CallTipsNoAutoCompletionContext); setCallTipsVisible(0); // This actually makes all of them visible } @@ -189,7 +189,6 @@ void ScriptEditor::enableAutoCompletion() { * */ void ScriptEditor::disableAutoCompletion() { setAutoCompletionSource(QsciScintilla::AcsNone); - setCallTipsVisible(QsciScintilla::CallTipsNone); setAutoCompletionThreshold(-1); setCallTipsVisible(-1); } -- GitLab