Skip to content
Snippets Groups Projects
Commit ee0b4f81 authored by Martyn Gigg's avatar Martyn Gigg
Browse files

Implement autocompletion and calltips in the editor.

Refs #21251
parent 309371f4
No related merge requests found
......@@ -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]
......
......@@ -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):
......
......@@ -115,4 +115,3 @@ class InputSplitter(IPyInputSplitter):
# General fallback - accept more code
return True
......@@ -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):
......
......@@ -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()
......
......@@ -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);
......
......@@ -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();
......
......@@ -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);
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment