Skip to content

Commit 0046e64

Browse files
authored
feat(cli): Support using LSP workspace folder as default project root for the LSP server. (#269)
* feat(cli): Support using LSP workspace folder as default project root * docs. * Auto generate docs * remove duplicate CI runs --------- Co-authored-by: Davidyz <[email protected]>
1 parent cd79d07 commit 0046e64

File tree

5 files changed

+62
-6
lines changed

5 files changed

+62
-6
lines changed

.github/workflows/panvimdoc.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
name: panvimdoc
22

33
on:
4-
push:
54
pull_request:
65

76
permissions:

doc/VectorCode-cli.txt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -808,8 +808,11 @@ Note that:
808808
1. For easier parsing, `--pipe` is assumed to be enabled in LSP mode;
809809
2. A `vectorcode.lock` file will be created in your `db_path` directory **if you’re using the bundled chromadb server**. Please do not delete it while a
810810
vectorcode process is running;
811-
3. The LSP server supports `vectorise`, `query` and `ls` subcommands. The other
812-
subcommands may be added in the future.
811+
3. The LSP server supports `vectorise`, `query`, `ls` and `files` subcommands. The other
812+
subcommands may be added in the future;
813+
4. If the `--project_root` parameter is not provided, the LSP server will try to use the
814+
workspace folders <https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_workspaceFolders>
815+
provided by the LSP client as the project root (if available).
813816

814817

815818
MCP SERVER ~

docs/cli.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -730,8 +730,11 @@ Note that:
730730
2. A `vectorcode.lock` file will be created in your `db_path` directory __if
731731
you're using the bundled chromadb server__. Please do not delete it while a
732732
vectorcode process is running;
733-
3. The LSP server supports `vectorise`, `query` and `ls` subcommands. The other
734-
subcommands may be added in the future.
733+
3. The LSP server supports `vectorise`, `query`, `ls` and `files` subcommands. The other
734+
subcommands may be added in the future;
735+
4. If the `--project_root` parameter is not provided, the LSP server will try to use the
736+
[workspace folders](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_workspaceFolders)
737+
provided by the LSP client as the project root (if available).
735738

736739
### MCP Server
737740

src/vectorcode/lsp_main.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import traceback
88
import uuid
99
from typing import cast
10+
from urllib.parse import urlparse
1011

1112
import shtab
1213
from chromadb.types import Where
@@ -88,6 +89,15 @@ async def execute_command(ls: LanguageServer, args: list[str]):
8889
parsed_args = await parse_cli_args(args)
8990
logger.info("Parsed command arguments: %s", parsed_args)
9091
if parsed_args.project_root is None:
92+
workspace_folders = ls.workspace.folders
93+
if len(workspace_folders.keys()) == 1:
94+
_, workspace_folder = workspace_folders.popitem()
95+
lsp_dir = urlparse(workspace_folder.uri).path
96+
if os.path.isdir(lsp_dir):
97+
logger.debug(f"Using LSP workspace {lsp_dir} as project root.")
98+
DEFAULT_PROJECT_ROOT = lsp_dir
99+
elif len(workspace_folders) > 1: # pragma: nocover
100+
logger.info("Too many LSP workspace folders. Ignoring them...")
91101
if DEFAULT_PROJECT_ROOT is not None:
92102
parsed_args.project_root = DEFAULT_PROJECT_ROOT
93103
logger.warning("Using DEFAULT_PROJECT_ROOT: %s", DEFAULT_PROJECT_ROOT)

tests/test_lsp.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from unittest.mock import AsyncMock, MagicMock, patch
44

55
import pytest
6+
from lsprotocol.types import WorkspaceFolder
67
from pygls.exceptions import JsonRpcInternalError, JsonRpcInvalidRequest
78
from pygls.server import LanguageServer
89

@@ -20,6 +21,7 @@ def mock_language_server():
2021
ls.progress.create_async = AsyncMock()
2122
ls.progress.begin = MagicMock()
2223
ls.progress.end = MagicMock()
24+
ls.workspace = MagicMock()
2325
return ls
2426

2527

@@ -92,7 +94,6 @@ async def test_execute_command_query_default_proj_root(
9294
patch("builtins.open", MagicMock()) as mock_open,
9395
):
9496
global DEFAULT_PROJECT_ROOT
95-
9697
mock_config.project_root = None
9798
mock_parse_cli_args.return_value = mock_config
9899
mock_get_query_result_files.return_value = ["/test/file.txt"]
@@ -115,6 +116,46 @@ async def test_execute_command_query_default_proj_root(
115116
mock_language_server.progress.end.assert_called()
116117

117118

119+
@pytest.mark.asyncio
120+
async def test_execute_command_query_workspace_dir(mock_language_server, mock_config):
121+
workspace_folder = WorkspaceFolder(uri="file:///dummy_dir", name="dummy_dir")
122+
with (
123+
patch(
124+
"vectorcode.lsp_main.parse_cli_args", new_callable=AsyncMock
125+
) as mock_parse_cli_args,
126+
patch("vectorcode.lsp_main.ClientManager"),
127+
patch("vectorcode.lsp_main.get_collection", new_callable=AsyncMock),
128+
patch(
129+
"vectorcode.lsp_main.build_query_results", new_callable=AsyncMock
130+
) as mock_get_query_result_files,
131+
patch("os.path.isfile", return_value=True),
132+
patch("os.path.isdir", return_value=True),
133+
patch("builtins.open", MagicMock()) as mock_open,
134+
):
135+
mock_language_server.workspace = MagicMock()
136+
mock_language_server.workspace.folders = {"dummy_dir": workspace_folder}
137+
mock_config.project_root = None
138+
mock_parse_cli_args.return_value = mock_config
139+
mock_get_query_result_files.return_value = ["/test/file.txt"]
140+
141+
# Configure the MagicMock object to return a string when read() is called
142+
mock_file = MagicMock()
143+
mock_file.__enter__.return_value.read.return_value = "{}" # Return valid JSON
144+
mock_open.return_value = mock_file
145+
146+
# Mock the merge_from method
147+
mock_config.merge_from = AsyncMock(return_value=mock_config)
148+
149+
result = await execute_command(mock_language_server, ["query", "test"])
150+
151+
assert isinstance(result, list)
152+
mock_language_server.progress.begin.assert_called()
153+
mock_language_server.progress.end.assert_called()
154+
assert (
155+
mock_get_query_result_files.call_args.args[1].project_root == "/dummy_dir"
156+
)
157+
158+
118159
@pytest.mark.asyncio
119160
async def test_execute_command_ls(mock_language_server, mock_config):
120161
mock_config.action = CliAction.ls

0 commit comments

Comments
 (0)