Repository: microsoft/ptvsd

Issue:
Add support for auto completion in console window



Discussion:
Got this working, just need to add tests.
Deferred until we debugger is deemed stable before introducing any new features.

Resolving patch:
diff --git a/debugger_protocol/messages/_requests.py b/debugger_protocol/messages/_requests.py
--- a/debugger_protocol/messages/_requests.py
+++ b/debugger_protocol/messages/_requests.py
@@ -83,6 +83,7 @@ class Capabilities(FieldsNamespace):
         Field('supportsSetExpression', bool),
         Field('supportsModulesRequest', bool),
         Field('supportsDebuggerProperties', bool),
+        Field('supportsCompletionsRequest', bool),
     ]
 
 
diff --git a/ptvsd/wrapper.py b/ptvsd/wrapper.py
--- a/ptvsd/wrapper.py
+++ b/ptvsd/wrapper.py
@@ -54,6 +54,22 @@
 #    print(s)
 #ipcjson._TRACE = ipcjson_trace
 
+#completion types.
+TYPE_IMPORT = '0'
+TYPE_CLASS = '1'
+TYPE_FUNCTION = '2'
+TYPE_ATTR = '3'
+TYPE_BUILTIN = '4'
+TYPE_PARAM = '5'
+TYPE_LOOK_UP = {
+    TYPE_IMPORT: 'module',
+    TYPE_CLASS: 'class',
+    TYPE_FUNCTION: 'function',
+    TYPE_ATTR: 'field',
+    TYPE_BUILTIN: 'keyword',
+    TYPE_PARAM: 'variable',
+}
+
 
 def NOOP(*args, **kwargs):
     pass
@@ -952,6 +968,7 @@ def _stop_event_loop(self):
 
 
 INITIALIZE_RESPONSE = dict(
+    supportsCompletionsRequest=True,
     supportsConditionalBreakpoints=True,
     supportsConfigurationDoneRequest=True,
     supportsDebuggerProperties=True,
@@ -2222,6 +2239,34 @@ def on_exceptionInfo(self, request, args):
                      'source': source},
         )
 
+    @async_handler
+    def on_completions(self, request, args):
+        text = args['text']
+        vsc_fid = args.get('frameId', None)
+
+        try:
+            pyd_tid, pyd_fid = self.frame_map.to_pydevd(vsc_fid)
+        except KeyError:
+            self.send_error_response(request)
+
+        cmd_args = '{}\t{}\t{}\t{}'.format(pyd_tid, pyd_fid, 'LOCAL', text)
+        _, _, resp_args = yield self.pydevd_request(
+            pydevd_comm.CMD_GET_COMPLETIONS,
+            cmd_args)
+
+        xml = self.parse_xml_response(resp_args)
+        targets = []
+        for item in list(xml.comp):
+            target = {}
+            target['label'] = unquote(item['p0'])
+            try:
+                target['type'] = TYPE_LOOK_UP[item['p3']]
+            except KeyError:
+                pass
+            targets.append(target)
+
+        self.send_response(request, targets=targets)
+
     # Custom ptvsd message
     def on_ptvsd_systemInfo(self, request, args):
         try:


Test patch:
diff --git a/tests/highlevel/test_messages.py b/tests/highlevel/test_messages.py
--- a/tests/highlevel/test_messages.py
+++ b/tests/highlevel/test_messages.py
@@ -2299,24 +2299,6 @@ def test_unsupported(self):
         self.assert_received(self.debugger, [])
 
 
-class CompletionsTests(NormalRequestTest, unittest.TestCase):
-
-    COMMAND = 'completions'
-
-    def test_unsupported(self):
-        with self.launched():
-            self.send_request(
-                text='spa',
-                column=3,
-            )
-            received = self.vsc.received
-
-        self.assert_vsc_received(received, [
-            self.expected_failure('Unknown command'),
-        ])
-        self.assert_received(self.debugger, [])
-
-
 ##################################
 # VSC events
 
diff --git a/tests/resources/system_tests/test_completions/attach_completions.py b/tests/resources/system_tests/test_completions/attach_completions.py
new file mode 100644
--- /dev/null
+++ b/tests/resources/system_tests/test_completions/attach_completions.py
@@ -0,0 +1,23 @@
+import sys
+import ptvsd
+
+ptvsd.enable_attach((sys.argv[1], sys.argv[2]))
+ptvsd.wait_for_attach()
+
+
+class SomeClass():
+    def __init__(self, someVar):
+        self.some_var = someVar
+
+    def do_someting(self):
+        someVariable = self.some_var
+        return someVariable
+
+
+def someFunction(someVar):
+    someVariable = someVar
+    return SomeClass(someVariable).do_someting()
+
+
+someFunction('value')
+print('done')
diff --git a/tests/resources/system_tests/test_completions/launch_completions.py b/tests/resources/system_tests/test_completions/launch_completions.py
new file mode 100644
--- /dev/null
+++ b/tests/resources/system_tests/test_completions/launch_completions.py
@@ -0,0 +1,16 @@
+class SomeClass():
+    def __init__(self, someVar):
+        self.some_var = someVar
+
+    def do_someting(self):
+        someVariable = self.some_var
+        return someVariable
+
+
+def someFunction(someVar):
+    someVariable = someVar
+    return SomeClass(someVariable).do_someting()
+
+
+someFunction('value')
+print('done')
diff --git a/tests/system_tests/test_completions.py b/tests/system_tests/test_completions.py
new file mode 100644
--- /dev/null
+++ b/tests/system_tests/test_completions.py
@@ -0,0 +1,294 @@
+import os
+import os.path
+
+from tests.helpers.resource import TestResources
+from . import (
+    lifecycle_handshake, LifecycleTestsBase, DebugInfo, PORT,
+)
+
+
+TEST_FILES = TestResources.from_module(__name__)
+
+
+class CompletionsTests(LifecycleTestsBase):
+
+    def run_test_completions(self, debug_info, bp_filename, bp_line, expected):
+        pathMappings = []
+        # Required to ensure sourceReference = 0
+        if (debug_info.starttype == 'attach'):
+            pathMappings.append({
+                'localRoot': debug_info.cwd,
+                'remoteRoot': debug_info.cwd
+            })
+        options = {
+            'debugOptions': ['RedirectOutput'],
+            'pathMappings': pathMappings
+        }
+        breakpoints = [{
+            'source': {
+                'path': bp_filename
+            },
+            'breakpoints': [{
+                'line': bp_line
+            }]
+        }]
+
+        with self.start_debugging(debug_info) as dbg:
+            session = dbg.session
+            with session.wait_for_event('stopped') as result:
+                (_, req_launch_attach, _, _, _, _,
+                 ) = lifecycle_handshake(session, debug_info.starttype,
+                                         options=options,
+                                         breakpoints=breakpoints)
+                req_launch_attach.wait()
+
+            event = result['msg']
+            tid = event.body['threadId']
+
+            req_stacktrace = session.send_request(
+                'stackTrace',
+                threadId=tid,
+            )
+            req_stacktrace.wait()
+            frames = req_stacktrace.resp.body['stackFrames']
+            frame_id = frames[0]['id']
+
+            req_completions = session.send_request(
+                'completions',
+                text='some',
+                frameId=int(frame_id),
+                column=1
+            )
+            req_completions.wait(timeout=2.0)
+            targets = req_completions.resp.body['targets']
+
+            # make a request with bad frame id
+            bad_req_completions = session.send_request(
+                'completions',
+                text='some',
+                frameId=int(1234),
+                column=1
+            )
+            bad_req_completions.wait(timeout=2.0)
+            bad_result = bad_req_completions.resp.success
+
+            session.send_request(
+                'continue',
+                threadId=tid,
+            )
+
+        targets.sort(key=lambda t: t['label'])
+        expected.sort(key=lambda t: t['label'])
+        self.assertEqual(targets, expected)
+        self.assertEqual(bad_result, False)
+
+    def run_test_outermost_scope(self, debug_info, filename, line):
+        self.run_test_completions(
+            debug_info,
+            bp_filename=filename,
+            bp_line=line,
+            expected=[
+                {
+                    'label': 'SomeClass',
+                    'type': 'class'
+                },
+                {
+                    'label': 'someFunction',
+                    'type': 'function'
+                }
+            ]
+        )
+
+    def run_test_in_function(self, debug_info, filename, line):
+        self.run_test_completions(
+            debug_info,
+            bp_filename=filename,
+            bp_line=line,
+            expected=[
+                {
+                    'label': 'SomeClass',
+                    'type': 'class'
+                },
+                {
+                    'label': 'someFunction',
+                    'type': 'function'
+                },
+                {
+                    'label': 'someVar',
+                    'type': 'field'
+                },
+                {
+                    'label': 'someVariable',
+                    'type': 'field'
+                }
+            ]
+        )
+
+    def run_test_in_method(self, debug_info, filename, line):
+        self.run_test_completions(
+            debug_info,
+            bp_filename=filename,
+            bp_line=line,
+            expected=[
+                {
+                    'label': 'SomeClass',
+                    'type': 'class'
+                },
+                {
+                    'label': 'someFunction',
+                    'type': 'function'
+                },
+                {
+                    'label': 'someVariable',
+                    'type': 'field'
+                }
+            ]
+        )
+
+
+class LaunchFileTests(CompletionsTests):
+    def _get_debug_info(self):
+        filename = TEST_FILES.resolve('launch_completions.py')
+        cwd = os.path.dirname(filename)
+        return DebugInfo(filename=filename, cwd=cwd)
+
+    def test_outermost_scope(self):
+        debug_info = self._get_debug_info()
+        self.run_test_outermost_scope(debug_info, debug_info.filename, 16)
+
+    def test_in_function(self):
+        debug_info = self._get_debug_info()
+        self.run_test_in_function(debug_info, debug_info.filename, 12)
+
+    def test_in_method(self):
+        debug_info = self._get_debug_info()
+        self.run_test_in_method(debug_info, debug_info.filename, 7)
+
+
+class LaunchModuleTests(CompletionsTests):
+    def _get_debug_info(self):
+        module_name = 'launch_completions'
+        filename = TEST_FILES.resolve('launch_completions.py')
+        env = TEST_FILES.env_with_py_path()
+        cwd = TEST_FILES.root
+        return DebugInfo(modulename=module_name, cwd=cwd, env=env), filename
+
+    def test_outermost_scope(self):
+        debug_info, filename = self._get_debug_info()
+        self.run_test_outermost_scope(debug_info, filename, 16)
+
+    def test_in_function(self):
+        debug_info, filename = self._get_debug_info()
+        self.run_test_in_function(debug_info, filename, 12)
+
+    def test_in_method(self):
+        debug_info, filename = self._get_debug_info()
+        self.run_test_in_method(debug_info, filename, 7)
+
+
+class ServerAttachTests(CompletionsTests):
+    def _get_debug_info(self):
+        filename = TEST_FILES.resolve('launch_completions.py')
+        cwd = os.path.dirname(filename)
+        argv = ['localhost', str(PORT)]
+        return DebugInfo(
+                filename=filename,
+                cwd=cwd,
+                starttype='attach',
+                argv=argv,
+            ), filename
+
+    def test_outermost_scope(self):
+        debug_info, filename = self._get_debug_info()
+        self.run_test_outermost_scope(debug_info, filename, 16)
+
+    def test_in_function(self):
+        debug_info, filename = self._get_debug_info()
+        self.run_test_in_function(debug_info, filename, 12)
+
+    def test_in_method(self):
+        debug_info, filename = self._get_debug_info()
+        self.run_test_in_method(debug_info, filename, 7)
+
+
+class ServerAttachModuleTests(CompletionsTests):
+    def _get_debug_info(self):
+        module_name = 'launch_completions'
+        filename = TEST_FILES.resolve('launch_completions.py')
+        env = TEST_FILES.env_with_py_path()
+        cwd = TEST_FILES.root
+        argv = ['localhost', str(PORT)]
+        return DebugInfo(
+                modulename=module_name,
+                env=env,
+                cwd=cwd,
+                argv=argv,
+                starttype='attach',
+            ), filename
+
+    def test_outermost_scope(self):
+        debug_info, filename = self._get_debug_info()
+        self.run_test_outermost_scope(debug_info, filename, 16)
+
+    def test_in_function(self):
+        debug_info, filename = self._get_debug_info()
+        self.run_test_in_function(debug_info, filename, 12)
+
+    def test_in_method(self):
+        debug_info, filename = self._get_debug_info()
+        self.run_test_in_method(debug_info, filename, 7)
+
+
+class PTVSDAttachTests(CompletionsTests):
+    def _get_debug_info(self):
+        filename = TEST_FILES.resolve('attach_completions.py')
+        cwd = os.path.dirname(filename)
+        argv = ['localhost', str(PORT)]
+        return DebugInfo(
+                filename=filename,
+                attachtype='import',
+                cwd=cwd,
+                starttype='attach',
+                argv=argv,
+            ), filename
+
+    def test_outermost_scope(self):
+        debug_info, filename = self._get_debug_info()
+        self.run_test_outermost_scope(debug_info, filename, 23)
+
+    def test_in_function(self):
+        debug_info, filename = self._get_debug_info()
+        self.run_test_in_function(debug_info, filename, 19)
+
+    def test_in_method(self):
+        debug_info, filename = self._get_debug_info()
+        self.run_test_in_method(debug_info, filename, 14)
+
+
+class PTVSDAttachModuleTests(CompletionsTests):
+    def _get_debug_info(self):
+        filename = TEST_FILES.resolve('attach_completions.py')
+        module_name = 'attach_completions'
+        env = TEST_FILES.env_with_py_path()
+        cwd = TEST_FILES.root
+        argv = ['localhost', str(PORT)]
+        return DebugInfo(
+                modulename=module_name,
+                env=env,
+                cwd=cwd,
+                argv=argv,
+                attachtype='import',
+                starttype='attach',
+            ), filename
+
+    def test_outermost_scope(self):
+        debug_info, filename = self._get_debug_info()
+        self.run_test_outermost_scope(debug_info, filename, 23)
+
+    def test_in_function(self):
+        debug_info, filename = self._get_debug_info()
+        self.run_test_in_function(debug_info, filename, 19)
+
+    def test_in_method(self):
+        debug_info, filename = self._get_debug_info()
+        self.run_test_in_method(debug_info, filename, 14)