Repository: manateelazycat/lsp-bridge
Pull request #1188: feat: add workspace folder support to copilot.
Author: login: mopemope
Created: 2025-03-28T14:10:47Z | Merged: 2025-03-29T08:45:59Z

This change modifies the code to provide the workspace folder information to Copilot, aiming to enhance the accuracy of Copilot's code completion suggestions.



Diff:
diff --git a/core/copilot.py b/core/copilot.py
index 0fbb5002de..b6d07cfb5e 100644
--- a/core/copilot.py
+++ b/core/copilot.py
@@ -46,13 +46,14 @@ def __init__(self):
         self.is_get_info = False
         self.wait_id = None
         self.accept_commands = {}
+        self.workspace_folders = {}
 
     def check_node_version(self):
         version = subprocess.check_output([self.node_path, '-v'], stderr=subprocess.STDOUT, universal_newlines=True).strip()
         major_version = int(version.split('.')[0].lstrip('v'))
         return major_version >= 20
 
-    def start_copilot(self):
+    def start_copilot(self, project_path=None):
         self.get_info()
 
         if self.is_run:
@@ -84,14 +85,31 @@ def start_copilot(self):
         self.dispatcher.start()
 
         self.wait_id = generate_request_id()
+        workspace_folders = []
+        if project_path is not None:
+            folder = {
+                "uri": path_to_uri(project_path),
+                "name": os.path.basename(project_path)
+            }
+            workspace_folders.append(folder)
+            self.workspace_folders[project_path] = folder
+
         self.sender.send_request('initialize', {
             'processId': os.getpid(),
+            "workspaceFolders": workspace_folders,
             'clientInfo': {
                 "name": "emacs",
                 "version": "lsp-bridge"
             },
             'capabilities': {
-                'workspace': {'workspaceFolders': True}
+                'workspace': {
+                    'workspaceFolders': True,
+                },
+                'window': {
+                    'showDocument': {
+                        'support': True,
+                    }
+                }
             },
             'initializationOptions': {
                 'editorInfo': {
@@ -260,6 +278,7 @@ def complete(self,  position, editor_mode, file_path, relative_path, tab_size, t
         if file_path in self.file_versions:
             self.file_versions[file_path] += 1
 
+        self.did_focus(file_path)
         self.sync_file(text, file_path, self.get_language_id(editor_mode))
         self.current_language_id = self.get_language_id(editor_mode)
 
@@ -339,3 +358,31 @@ def logout(self):
         self.start_copilot()
         self.sender.send_request('signOut', {"dummy": "signOut"}, generate_request_id())
         message_emacs('Logged out')
+
+    def change_workspace_folder(self, project_path):
+        self.start_copilot(project_path=project_path)
+        if not self.is_initialized:
+            return
+        if project_path not in self.workspace_folders:
+            folder = {
+                "uri": path_to_uri(project_path),
+                "name": os.path.basename(project_path)
+            }
+            self.workspace_folders[project_path] = folder
+            # notify the server
+            self.sender.send_notification('workspace/didChangeWorkspaceFolders', {
+                "event": {
+                    "added": [folder],
+                    "removed": []
+                }
+            })
+
+    def did_focus(self, file_path):
+        if not self.is_initialized:
+            return
+
+        self.sender.send_notification('textDocument/didFocus', {
+            "textDocument": {
+                "uri": path_to_uri(file_path),
+            }
+        })
diff --git a/lsp_bridge.py b/lsp_bridge.py
index 61e98c5ec9..9b9cef2a5c 100755
--- a/lsp_bridge.py
+++ b/lsp_bridge.py
@@ -678,6 +678,9 @@ def open_file(self, filepath):
         project_path = get_project_path(filepath)
         multi_lang_server = get_emacs_func_result("get-multi-lang-server", project_path, filepath)
 
+        # notify change workspace folder to copilot server
+        self.copilot.change_workspace_folder(project_path)
+
         if os.path.splitext(filepath)[-1] == '.org':
             single_lang_server = get_emacs_func_result("get-single-lang-server", project_path, filepath)
             lang_server_info = load_single_server_info(single_lang_server)


Reviews:
user: login: manateelazycat
state: CHANGES_REQUESTED
author_association: OWNER
submitted_at: 2025-03-29T06:36:07Z
user: login: mopemope
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-29T08:43:38Z
user: login: kira-bruneau
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-30T19:01:37Z

Review comments:
diff_hunk: @@ -678,6 +678,9 @@ def open_file(self, filepath):
         project_path = get_project_path(filepath)
         multi_lang_server = get_emacs_func_result("get-multi-lang-server", project_path, filepath)
 
+        # notify change workspace folder to copilot server
+        self.copilot.change_workspace_folder(project_path)
+
path: lsp_bridge.py
user: login: manateelazycat
body: If user don't install copilot, this function will failed at Python side?
created_at: 2025-03-29T06:35:58Z
author_association: OWNER
diff_hunk: @@ -678,6 +678,9 @@ def open_file(self, filepath):
         project_path = get_project_path(filepath)
         multi_lang_server = get_emacs_func_result("get-multi-lang-server", project_path, filepath)
 
+        # notify change workspace folder to copilot server
+        self.copilot.change_workspace_folder(project_path)
+
path: lsp_bridge.py
user: login: mopemope
body: No, it's fine.
If Copilot has not been initialized, this function does nothing.

```python
    def change_workspace_folder(self, project_path):
        self.start_copilot(project_path=project_path)
        if not self.is_initialized:
            return
```
created_at: 2025-03-29T08:43:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -678,6 +678,9 @@ def open_file(self, filepath):
         project_path = get_project_path(filepath)
         multi_lang_server = get_emacs_func_result("get-multi-lang-server", project_path, filepath)
 
+        # notify change workspace folder to copilot server
+        self.copilot.change_workspace_folder(project_path)
+
path: lsp_bridge.py
user: login: kira-bruneau
body: Oh oops, I missed this before commenting.

This will still try to start copilot even if you don't use it, and it will break `lsp-bridge` if you don't have nodejs installed.
created_at: 2025-03-30T19:01:37Z
author_association: CONTRIBUTOR

PR comments:
user: login: manateelazycat
created_at: 2025-03-29T08:46:14Z
body: @mopemope Cool, thanks for patch.
author_association: OWNER
user: login: kira-bruneau
created_at: 2025-03-30T18:54:43Z
body: I noticed that this change implicitly adds a dependency on nodejs. Most of the tests fail now if you don't have it installed.

I'm not too familiar with the code, but should a check be added to make sure copilot has been initalized before trying to call `change_workspace_folder`?

```
Eval in Emacs: (lsp-bridge-search-backend--record-items '"search-file-words" '())
Traceback (most recent call last):
  File "/build/source/lsp_bridge.py", line 650, in event_dispatcher
    getattr(self, func_name)(*func_args)
  File "/build/source/lsp_bridge.py", line 934, in _do
    open_file_success = self.open_file(filepath)  # _do is called inside event_loop, so we can block here.
                        ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/build/source/lsp_bridge.py", line 682, in open_file
    self.copilot.change_workspace_folder(project_path)
  File "/build/source/core/copilot.py", line 363, in change_workspace_folder
    self.start_copilot(project_path=project_path)
  File "/build/source/core/copilot.py", line 63, in start_copilot
    if not self.check_node_version():
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/build/source/core/copilot.py", line 52, in check_node_version
    version = subprocess.check_output([self.node_path, '-v'], stderr=subprocess.STDOUT, universal_newlines=True).strip()
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/f2krmq3iv5nibcvn4rw7nrnrciqprdkh-python3-3.12.9/lib/python3.12/subprocess.py", line 468, in check_output
    return run(*popenargs, stdout=PIPE, timeout=timeout, check=True,
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/f2krmq3iv5nibcvn4rw7nrnrciqprdkh-python3-3.12.9/lib/python3.12/subprocess.py", line 550, in run
    with Popen(*popenargs, **kwargs) as process:
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/f2krmq3iv5nibcvn4rw7nrnrciqprdkh-python3-3.12.9/lib/python3.12/subprocess.py", line 1028, in __init__
    self._execute_child(args, executable, preexec_fn, close_fds,
  File "/nix/store/f2krmq3iv5nibcvn4rw7nrnrciqprdkh-python3-3.12.9/lib/python3.12/subprocess.py", line 1963, in _execute_child
    raise child_exception_type(errno_num, err_msg, err_filename)
FileNotFoundError: [Errno 2] No such file or directory: 'node'
```

```
FAIL: test_python (test.test_completion.SimpleCompletion.test_python)

----------------------------------------------------------------------

Traceback (most recent call last):

  File "/build/source/test/test_completion.py", line 38, in test_python

    try_complete(SingleFile(

  File "/build/source/test/test_completion.py", line 33, in try_complete

    return complete_file()

           ^^^^^^^^^^^^^^^

  File "/build/source/test/common.py", line 131, in wrapper

    func(*args, **kwargs, filename=_buffer_file_name(t_file.name))

  File "/build/source/test/common.py", line 101, in wrapper

    assert ok

           ^^

AssertionError



======================================================================

FAIL: test_python_with_utf8 (test.test_completion.SimpleCompletion.test_python_with_utf8)

----------------------------------------------------------------------

Traceback (most recent call last):

  File "/build/source/test/test_completion.py", line 45, in test_python_with_utf8

    try_complete(SingleFile(

  File "/build/source/test/test_completion.py", line 33, in try_complete

    return complete_file()

           ^^^^^^^^^^^^^^^

  File "/build/source/test/common.py", line 131, in wrapper

    func(*args, **kwargs, filename=_buffer_file_name(t_file.name))

  File "/build/source/test/common.py", line 101, in wrapper

    assert ok

           ^^

AssertionError



======================================================================

FAIL: test_jump_external (test.test_find_define.JumpOtherFile.test_jump_external)

----------------------------------------------------------------------

Traceback (most recent call last):

  File "/build/source/test/test_find_define.py", line 130, in test_jump_external

    result_file = self.jump(files, "b.py", get_offset(files[0].code, "os"))

                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

  File "/build/source/test/test_find_define.py", line 102, in jump

    go()

  File "/build/source/test/common.py", line 151, in wrapper

    func(*args, **kwargs, filenames=d)

  File "/build/source/test/common.py", line 101, in wrapper

    assert ok

           ^^

AssertionError



======================================================================

FAIL: test_jump_project (test.test_find_define.JumpOtherFile.test_jump_project)

----------------------------------------------------------------------

Traceback (most recent call last):

  File "/build/source/test/test_find_define.py", line 119, in test_jump_project

    result_file = self.jump(files, "b.py", get_offset(files[0].code, "a)"))

                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

  File "/build/source/test/test_find_define.py", line 102, in jump

    go()

  File "/build/source/test/common.py", line 151, in wrapper

    func(*args, **kwargs, filenames=d)

  File "/build/source/test/common.py", line 101, in wrapper

    assert ok

           ^^

AssertionError



----------------------------------------------------------------------
```
author_association: CONTRIBUTOR
user: login: kira-bruneau
created_at: 2025-03-30T19:09:11Z
body: I opened a PR here to fix this: #1190
author_association: CONTRIBUTOR