diff --git a/docs/source/release/v4.2.0/mantidworkbench.rst b/docs/source/release/v4.2.0/mantidworkbench.rst
index 805c4a3d6c48f29b7aed07e710473c0a459548ab..4469da4c126872d07545cb1bd89837d5b190fe64 100644
--- a/docs/source/release/v4.2.0/mantidworkbench.rst
+++ b/docs/source/release/v4.2.0/mantidworkbench.rst
@@ -37,6 +37,7 @@ Improvements
 - It is now possible to fit table workspaces in the fit browser and in a script.
 - It is now possible to input axis limits in the figure options using scientific notation.
 - The sub-tabs in the Curves tab in plot options now contain an "Apply to All" button which copies the properties of the current curve to all other curves in the plot.
+- The auto-complete in Workbench's script editor has been improved.
 
 Bugfixes
 ########
diff --git a/qt/python/CMakeLists.txt b/qt/python/CMakeLists.txt
index 8aabf85e81974b1f882a997ad0fc40b20edcc4cb..51db6aea2d29e2a4c6e9de519fbd1ded76ed05a4 100644
--- a/qt/python/CMakeLists.txt
+++ b/qt/python/CMakeLists.txt
@@ -92,6 +92,7 @@ if(ENABLE_WORKBENCH OR ENABLE_WORKBENCH)
     mantidqt/utils/test/test_qt_utils.py
     mantidqt/utils/test/test_writetosignal.py
     mantidqt/widgets/codeeditor/test/test_codeeditor.py
+    mantidqt/widgets/codeeditor/test/test_completion.py
     mantidqt/widgets/codeeditor/test/test_errorformatter.py
     mantidqt/widgets/codeeditor/test/test_execution.py
     mantidqt/widgets/codeeditor/test/test_interpreter.py
diff --git a/qt/python/mantidqt/widgets/codeeditor/completion.py b/qt/python/mantidqt/widgets/codeeditor/completion.py
new file mode 100644
index 0000000000000000000000000000000000000000..85f4041f72198f7571bb3039c59d1471141ab57a
--- /dev/null
+++ b/qt/python/mantidqt/widgets/codeeditor/completion.py
@@ -0,0 +1,252 @@
+# Mantid Repository : https://github.com/mantidproject/mantid
+#
+# Copyright © 2019 ISIS Rutherford Appleton Laboratory UKRI,
+#     NScD Oak Ridge National Laboratory, European Spallation Source
+#     & Institut Laue - Langevin
+# SPDX - License - Identifier: GPL - 3.0 +
+#  This file is part of the mantid workbench.
+"""
+There was some time spent looking into using jedi for the completions.
+jedi performs static analysis on code, which aims to provide completions
+from modules and objects without needing to run any code. Because of
+this, jedi needed to be updated almost every time the cursor position
+was changed. This reduced the script editors performance especially when
+analyzing large modules like numpy (not to an unusable level, but it was
+noticeable).
+
+Further, jedi could not statically pick-up many of the simpleapi
+algorithms, which are defined dynamically, so we had to provide these
+ourselves anyway. jedi also caused workbench to crash when launching
+through PyCharm's debugger using Python 2 (it ran fine when launched
+normally - I think the cause was something to do with the debugging
+process interfering with jedi's subprocess).
+
+For these reasons it was agreed jedi would be dropped, possibly
+revisiting when we move to Python 3.
+"""
+
+from __future__ import (absolute_import, unicode_literals)
+
+import ast
+import inspect
+import re
+import sys
+from keyword import kwlist as python_keywords
+from collections import namedtuple
+from six import PY2
+if PY2:  # noqa
+    from inspect import getargspec as getfullargspec
+else:  # noqa
+    from inspect import getfullargspec
+
+from mantidqt.widgets.codeeditor.editor import CodeEditor
+
+ArgSpec = namedtuple("ArgSpec", "args varargs keywords defaults")
+
+
+def get_builtin_argspec(builtin):
+    """
+    Get the call tips for a builtin function from its docstring
+    :param builtin builtin: The builtin to generate call tips for
+    :return inspect.ArgSpec: The ArgSpec of the builtin. If no function
+        description is available in the docstring return None
+    """
+    doc_string = builtin.__doc__
+    if not doc_string:
+        return None
+    func_descriptor = doc_string.split('\n')[0].strip()
+    if re.search(builtin.__name__ + "\([\[\*a-zA-Z_].*\)", func_descriptor):
+        args_string = func_descriptor[func_descriptor.find('(') + 1:func_descriptor.rfind(')')]
+        all_args_list = args_string.split(', ')
+        args = []
+        defaults = []
+        for arg in all_args_list:
+            if '=' in arg:
+                args.append(arg.split('=')[0].strip())
+                defaults.append(arg.split('=')[1].strip())
+            else:
+                args.append(arg)
+        arg_spec = ArgSpec(args, None, None, defaults)
+        return arg_spec
+
+
+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:
+        try:
+            args_obj = inspect.getargs(func.__code__)
+            argspec = ArgSpec(args_obj.args, args_obj.varargs, args_obj.varkw, defaults=None)
+        except (TypeError, AttributeError, ValueError):
+            if inspect.isbuiltin(func):
+                argspec = get_builtin_argspec(func)
+                if not argspec:
+                    return ''
+            else:
+                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').replace(',', ', ')
+        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:
+        call_tip = "({})".format(', '.join(args))
+    else:
+        # The defaults list contains the default values for the last n arguments
+        diff = len(args) - len(defs)
+        call_tip = ''
+        for index in range(len(args) - 1, -1, -1):
+            def_index = index - diff
+            if def_index >= 0:
+                call_tip = '[' + args[index] + '], ' + call_tip
+            else:
+                call_tip = args[index] + ", " + call_tip
+        call_tip = '(' + call_tip.rstrip(', ') + ')'
+    return call_tip
+
+
+def generate_call_tips(definitions, prepend_module_name=None):
+    """
+    Generate call tips for a dictionary of object definitions (eg. globals()).
+    The call tips generated are of the form:
+        Load(InputWorkspace, [OutputWorkspace], ...)
+    where squared braces denote key-word arguments.
+
+    :param dict definitions: Dictionary with names of python objects as keys and
+        the objects themselves as values
+    :param prepend_module_name: bool, None, or str. Prepend the name of the module to the call tips
+        default is None, if string given that string will act as the module name
+    :returns list: A list of call tips
+    """
+    if not isinstance(definitions, dict):
+        return []
+    call_tips = []
+    for name, py_object in definitions.items():
+        if name.startswith('_'):
+            continue
+        if prepend_module_name is True:
+            prepend_module_name = py_object.__module__
+        if inspect.isfunction(py_object) or inspect.isbuiltin(py_object):
+            if not prepend_module_name:
+                call_tips.append(name + get_function_spec(py_object))
+            else:
+                call_tips.append(prepend_module_name + '.' + name + get_function_spec(py_object))
+            continue
+        # Ignore modules or we get duplicates of methods/classes that are imported
+        # in outer scopes, e.g. numpy.array and numpy.core.array
+        if inspect.ismodule(py_object):
+            continue
+        for attr in dir(py_object):
+            try:
+                f_attr = getattr(py_object, attr)
+            except Exception:
+                continue
+            if attr.startswith('_'):
+                continue
+            if hasattr(f_attr, 'im_func') or inspect.isfunction(f_attr) or inspect.ismethod(f_attr):
+                call_tip = name + '.' + attr + get_function_spec(f_attr)
+            else:
+                call_tip = name + '.' + attr
+            if prepend_module_name:
+                call_tips.append(prepend_module_name + '.' + call_tip)
+    return call_tips
+
+
+def get_line_number_from_index(string, index):
+    return string[:index].count('\n')
+
+
+def get_module_import_alias(import_name, text):
+    for node in ast.walk(ast.parse(text)):
+        if isinstance(node, ast.alias) and node.name == import_name:
+            return node.asname
+    return import_name
+
+
+class CodeCompleter(object):
+    """
+    This class generates autocompletions for Workbench's script editor.
+    It generates autocompletions from environment globals. These completions
+    are updated on every successful script execution.
+    """
+    def __init__(self, editor, env_globals=None):
+        self.editor = editor
+        self.env_globals = env_globals
+
+        # A dict gives O(1) lookups and ensures we have no duplicates
+        self._completions_dict = dict()
+        if "from mantid.simpleapi import *" in self.editor.text():
+            self._add_to_completions(self._get_module_call_tips('mantid.simpleapi'))
+        if re.search("^#{0}import .*numpy( |,|$)", self.editor.text(), re.MULTILINE):
+            self._add_to_completions(self._get_module_call_tips('numpy'))
+        if re.search("^#{0}import .*pyplot( |,|$)", self.editor.text(), re.MULTILINE):
+            self._add_to_completions(self._get_module_call_tips('matplotlib.pyplot'))
+        self._add_to_completions(python_keywords)
+
+        self.editor.enableAutoCompletion(CodeEditor.AcsAPIs)
+        self.editor.updateCompletionAPI(self.completions)
+
+    @property
+    def completions(self):
+        return list(self._completions_dict.keys())
+
+    def _get_completions_from_globals(self):
+        return generate_call_tips(self.env_globals, prepend_module_name=True)
+
+    def _add_to_completions(self, completions):
+        for completion in completions:
+            self._completions_dict[completion] = True
+
+    def update_completion_api(self):
+        self._add_to_completions(self._get_completions_from_globals())
+        self.editor.updateCompletionAPI(self.completions)
+
+    def _get_module_call_tips(self, module):
+        """
+        Get the call tips for a given module. If the module cannot be
+        found in sys.modules return an empty list
+        :param str module: The name of the module
+        :return list: A list of call tips for the module
+        """
+        try:
+            module = sys.modules[module]
+        except KeyError:
+            return []
+        module_name = get_module_import_alias(module.__name__, self.editor.text())
+        return generate_call_tips(module.__dict__, module_name)
diff --git a/qt/python/mantidqt/widgets/codeeditor/execution.py b/qt/python/mantidqt/widgets/codeeditor/execution.py
index f8b8c733da11c70a46ff4b69f8e3986e57c95dd7..2b02b55a5c87b26100cf33e59d1c7559ba18a377 100644
--- a/qt/python/mantidqt/widgets/codeeditor/execution.py
+++ b/qt/python/mantidqt/widgets/codeeditor/execution.py
@@ -9,90 +9,20 @@
 #
 from __future__ import absolute_import
 
-import inspect
 import os
 
 from qtpy.QtCore import QObject, Signal
 from qtpy.QtWidgets import QApplication
-from six import PY2, iteritems
 
 from mantidqt.utils import AddedToSysPath
 from mantidqt.utils.asynchronous import AsyncTask, BlockingAsyncTaskWithCallback
 from mantidqt.widgets.codeeditor.inputsplitter import InputSplitter
 
-if PY2:
-    from inspect import getargspec as getfullargspec
-else:
-    from inspect import getfullargspec
-
 EMPTY_FILENAME_ID = '<string>'
 FILE_ATTR = '__file__'
 COMPILE_MODE = 'exec'
 
 
-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
     strings of Python code. It supports
@@ -171,20 +101,6 @@ class PythonCodeExecution(QObject):
                                    dont_inherit=True)
                 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._namespace = {}, {}
diff --git a/qt/python/mantidqt/widgets/codeeditor/interpreter.py b/qt/python/mantidqt/widgets/codeeditor/interpreter.py
index 9c6e32d87b03dd018bc720bcf40d2884387eea88..5ea63ad74c3ecbd584dfca2d87f52f6f01f3e8a8 100644
--- a/qt/python/mantidqt/widgets/codeeditor/interpreter.py
+++ b/qt/python/mantidqt/widgets/codeeditor/interpreter.py
@@ -17,6 +17,7 @@ from qtpy.QtWidgets import QFileDialog, QMessageBox, QStatusBar, QVBoxLayout, QW
 
 from mantidqt.io import open_a_file_dialog
 from mantidqt.widgets.codeeditor.codecommenter import CodeCommenter
+from mantidqt.widgets.codeeditor.completion import CodeCompleter
 from mantidqt.widgets.codeeditor.editor import CodeEditor
 from mantidqt.widgets.codeeditor.errorformatter import ErrorFormatter
 from mantidqt.widgets.codeeditor.execution import PythonCodeExecution
@@ -136,6 +137,7 @@ class PythonFileInterpreter(QWidget):
 
         self._presenter = PythonFileInterpreterPresenter(self, PythonCodeExecution(content))
         self.code_commenter = CodeCommenter(self.editor)
+        self.code_completer = CodeCompleter(self.editor, self._presenter.model.globals_ns)
 
         self.editor.modificationChanged.connect(self.sig_editor_modified)
         self.editor.fileNameChanged.connect(self.sig_filename_modified)
@@ -147,6 +149,9 @@ class PythonFileInterpreter(QWidget):
         self._presenter.model.sig_exec_error.connect(self.sig_exec_error)
         self._presenter.model.sig_exec_success.connect(self.sig_exec_success)
 
+        # Re-populate the completion API after execution success
+        self._presenter.model.sig_exec_success.connect(self.code_completer.update_completion_api)
+
     def closeEvent(self, event):
         self.deleteLater()
         if self.find_replace_dialog:
@@ -255,8 +260,6 @@ class PythonFileInterpreter(QWidget):
         # Default content does not count as a modification
         editor.setModified(False)
 
-        editor.enableAutoCompletion(CodeEditor.AcsAll)
-
     def clear_key_binding(self, key_str):
         """Clear a keyboard shortcut bound to a Scintilla command"""
         self.editor.clearKeyBinding(key_str)
@@ -276,9 +279,6 @@ class PythonFileInterpreterPresenter(QObject):
         self._is_executing = False
         self._error_formatter = ErrorFormatter()
 
-        # If startup code was executed then populate autocomplete
-        self.view.editor.updateCompletionAPI(self.model.generate_calltips())
-
         # connect signals
         self.model.sig_exec_success.connect(self._on_exec_success)
         self.model.sig_exec_error.connect(self._on_exec_error)
@@ -329,7 +329,6 @@ 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, task_result=task_result)
 
     def _on_exec_error(self, task_error):
diff --git a/qt/python/mantidqt/widgets/codeeditor/test/test_completion.py b/qt/python/mantidqt/widgets/codeeditor/test/test_completion.py
new file mode 100644
index 0000000000000000000000000000000000000000..0d02677ee05f8b9b711f4de2b366ac05ca0cde61
--- /dev/null
+++ b/qt/python/mantidqt/widgets/codeeditor/test/test_completion.py
@@ -0,0 +1,106 @@
+# Mantid Repository : https://github.com/mantidproject/mantid
+#
+# Copyright &copy; 2019 ISIS Rutherford Appleton Laboratory UKRI,
+#     NScD Oak Ridge National Laboratory, European Spallation Source
+#     & Institut Laue - Langevin
+# SPDX - License - Identifier: GPL - 3.0 +
+#  This file is part of the mantid workbench.
+from __future__ import (absolute_import, unicode_literals)
+
+import re
+import unittest
+
+import matplotlib.pyplot as plt  # noqa
+import numpy as np  # noqa
+
+from mantid.simpleapi import Rebin  # noqa  # needed so sys.modules can pick up Rebin
+from mantid.py3compat.mock import Mock
+from mantidqt.widgets.codeeditor.completion import (CodeCompleter, get_function_spec,
+                                                    get_builtin_argspec, get_module_import_alias)
+from testhelpers import assertRaisesNothing
+
+
+class CodeCompletionTest(unittest.TestCase):
+
+    def _get_completer(self, text, env_globals=None):
+        return CodeCompleter(Mock(text=lambda: text, fileName=lambda: ""), env_globals)
+
+    def _run_check_call_tip_generated(self, script_text, call_tip_regex):
+        completer = self._get_completer(script_text)
+        update_completion_api_mock = completer.editor.updateCompletionAPI
+        call_tips = update_completion_api_mock.call_args_list[0][0][0]
+        self.assertEqual(1, update_completion_api_mock.call_count)
+        self.assertGreater(len(call_tips), 1)
+        self.assertTrue(re.search(call_tip_regex, ' '.join(call_tips)))
+
+    def _run_check_call_tip_not_generated(self, script_text, call_tip_regex):
+        completer = self._get_completer(script_text)
+        update_completion_api_mock = completer.editor.updateCompletionAPI
+        call_tips = update_completion_api_mock.call_args_list[0][0][0]
+        self.assertEqual(1, update_completion_api_mock.call_count)
+        self.assertFalse(bool(re.search(call_tip_regex, ' '.join(call_tips))))
+
+    def test_Rebin_call_tips_generated_on_construction_when_api_import_in_script(self):
+        self._run_check_call_tip_generated("from mantid.simpleapi import *\n# My code",
+                                           "Rebin\(InputWorkspace, .*\)")
+
+    def test_numpy_call_tips_generated_if_numpy_imported_in_script(self):
+        self._run_check_call_tip_generated("import numpy as np\n# My code",
+                                           "np\.asarray\(a, \[dtype\], .*\)")
+
+    def test_pyplot_call_tips_generated_if_imported_in_script(self):
+        self._run_check_call_tip_generated("import matplotlib.pyplot as plt\n# My code",
+                                           "plt\.figure\(\[num\], .*\)")
+
+    def test_simple_api_call_tips_not_generated_on_construction_if_api_import_not_in_script(self):
+        self._run_check_call_tip_not_generated("import numpy as np\n# My code", "Rebin")
+
+    def test_numpy_call_tips_not_generated_if_its_not_imported(self):
+        self._run_check_call_tip_not_generated("# My code", "numpy")
+
+    def test_pyplot_call_tips_not_generated_if_its_not_imported(self):
+        self._run_check_call_tip_not_generated("# My code", "pyplot")
+
+    def test_nothing_raised_when_getting_completions_from_a_not_imported_module(self):
+        completer = self._get_completer("# My code")
+        assertRaisesNothing(self, completer._get_module_call_tips, 'this.doesnt.exist')
+
+    def test_get_function_spec_returns_expected_string_for_explicit_args(self):
+        def my_new_function(arg1, arg2, kwarg1=None, kwarg2=0):
+            pass
+
+        self.assertEqual("(arg1, arg2, [kwarg1], [kwarg2])", get_function_spec(my_new_function))
+
+    def test_get_function_spec_returns_expected_string_for_implicit_args(self):
+        def my_new_function(*args, **kwargs):
+            pass
+
+        self.assertEqual("(args, [**kwargs])", get_function_spec(my_new_function))
+
+    def test_get_builtin_argspec_generates_argspec_for_numpy_builtin(self):
+        argspec = get_builtin_argspec(np.zeros)
+        self.assertIn("shape, dtype, order", ', '.join(argspec.args))
+        self.assertIn("float, 'C'", ', '.join(argspec.defaults))
+
+    def test_get_module_import_alias_finds_import_aliases(self):
+        script = ("import numpy as np\n"
+                  "from keyword import kwlist as key_word_list\n"
+                  "import matplotlib.pyplot as plt\n"
+                  "import mymodule.some_func as func, something as _smthing\n"
+                  "# import commented.module as not_imported\n"
+                  "import thing as _thing # import kwlist2 as kew_word_list2")
+        aliases = {
+            'numpy': 'np',
+            'kwlist': 'key_word_list',
+            'matplotlib.pyplot': 'plt',
+            'mymodule.some_func': 'func',
+            'something': '_smthing',
+            'commented.module': 'commented.module',
+            'kwlist2': 'kwlist2'  # alias is commented out so expect alias to not be assigned
+        }
+        for import_name, alias in aliases.items():
+            self.assertEqual(alias, get_module_import_alias(import_name, script))
+
+
+if __name__ == '__main__':
+    unittest.main()