From 64d8cd70dbd748ec2675e58e6e2987d2469c70f2 Mon Sep 17 00:00:00 2001 From: Ankit Chouhan Date: Wed, 24 Jun 2026 18:17:22 +0530 Subject: [PATCH 1/4] Support multi-root Pydance workspaces --- pydance/package-lock.json | 4 +- pydance/package.json | 8 +- pydance/src/extension.ts | 33 +++++--- pylight/Cargo.lock | 2 +- pylight/Cargo.toml | 2 +- pylight/src/index/updater.rs | 53 ++++++++---- pylight/src/lsp/server.rs | 151 +++++++++++++++++++++++++---------- pylight/src/watcher.rs | 26 +----- 8 files changed, 180 insertions(+), 99 deletions(-) diff --git a/pydance/package-lock.json b/pydance/package-lock.json index 86071c1..bf262b4 100644 --- a/pydance/package-lock.json +++ b/pydance/package-lock.json @@ -1,12 +1,12 @@ { "name": "pydance", - "version": "0.2.1", + "version": "0.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pydance", - "version": "0.2.1", + "version": "0.3.1", "license": "ISC", "dependencies": { "vscode-languageclient": "^9.0.1" diff --git a/pydance/package.json b/pydance/package.json index fe25458..f64ee28 100644 --- a/pydance/package.json +++ b/pydance/package.json @@ -2,7 +2,7 @@ "name": "pydance", "displayName": "Pydance", "description": "Python language server that provides high-performance workspace symbol search for large Python codebases", - "version": "0.3.0", + "version": "0.3.1", "publisher": "ToughType", "icon": "images/pydance-logo.png", "repository": { @@ -20,6 +20,12 @@ ], "main": "./out/extension.js", "contributes": { + "commands": [ + { + "command": "pydance.restartServer", + "title": "Pydance: Restart Server" + } + ], "configuration": { "title": "Pydance", "properties": { diff --git a/pydance/src/extension.ts b/pydance/src/extension.ts index 0e670ad..e35bef7 100644 --- a/pydance/src/extension.ts +++ b/pydance/src/extension.ts @@ -7,7 +7,19 @@ import { Trace, } from "vscode-languageclient/node"; -let client: LanguageClient; +let client: LanguageClient | undefined; +let serverOptions: ServerOptions; +let clientOptions: LanguageClientOptions; +let trace: Trace = Trace.Off; + +async function restartServer() { + if (client) { + await client.stop(); + } + client = new LanguageClient("pydance", "Pydance", serverOptions, clientOptions); + client.setTrace(trace); + await client.start(); +} export function activate(context: vscode.ExtensionContext) { const outputChannel = vscode.window.createOutputChannel("Pydance"); @@ -23,13 +35,13 @@ export function activate(context: vscode.ExtensionContext) { const excludePatterns = config.get("excludePatterns", []); outputChannel.appendLine(`Using parser: ${parser}`); // If the extension is launched in debug mode then the debug server options are used - const serverOptions: ServerOptions = { + serverOptions = { run: { command: serverPath, args: ["--parser", parser] }, debug: { command: serverPath, args: ["--parser", parser] }, }; // Options to control the language client - const clientOptions: LanguageClientOptions = { + clientOptions = { // Register the server for Python documents documentSelector: [{ scheme: "file", language: "python" }], outputChannel: outputChannel, @@ -40,13 +52,7 @@ export function activate(context: vscode.ExtensionContext) { }; // Create the language client and start the client. - client = new LanguageClient( - "pydance", - "Pydance", - // serverOptions, - serverOptions, - clientOptions - ); + client = new LanguageClient("pydance", "Pydance", serverOptions, clientOptions); // Set trace level based on configuration const traceMap: { [key: string]: Trace } = { @@ -54,7 +60,12 @@ export function activate(context: vscode.ExtensionContext) { messages: Trace.Messages, verbose: Trace.Verbose, }; - client.setTrace(traceMap[traceLevel] || Trace.Off); + trace = traceMap[traceLevel] || Trace.Off; + client.setTrace(trace); + + context.subscriptions.push( + vscode.commands.registerCommand("pydance.restartServer", restartServer) + ); outputChannel.appendLine("Starting language client..."); // Start the client. This will also launch the server diff --git a/pylight/Cargo.lock b/pylight/Cargo.lock index 88997db..62a9e4c 100644 --- a/pylight/Cargo.lock +++ b/pylight/Cargo.lock @@ -1515,7 +1515,7 @@ dependencies = [ [[package]] name = "pylight" -version = "0.3.0" +version = "0.3.1" dependencies = [ "anyhow", "async-walkdir", diff --git a/pylight/Cargo.toml b/pylight/Cargo.toml index 66fcab3..8ba73be 100644 --- a/pylight/Cargo.toml +++ b/pylight/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pylight" -version = "0.3.0" +version = "0.3.1" edition = "2021" diff --git a/pylight/src/index/updater.rs b/pylight/src/index/updater.rs index 10f8029..c76eda9 100644 --- a/pylight/src/index/updater.rs +++ b/pylight/src/index/updater.rs @@ -22,27 +22,43 @@ enum UpdaterState { pub struct IndexUpdater { index: Arc, state: Arc>, - workspace_root: PathBuf, - ignore_filter: Arc, + workspace_roots: Vec, + ignore_filters: Vec>, } impl IndexUpdater { pub fn new(index: Arc, workspace_root: PathBuf) -> Self { - let ignore_filter = Arc::new(crate::file_filter::IgnoreFilter::new( - workspace_root.clone(), - )); + Self::new_multi(index, vec![workspace_root]) + } + + pub fn new_multi(index: Arc, workspace_roots: Vec) -> Self { + let ignore_filters = workspace_roots + .iter() + .cloned() + .map(crate::file_filter::IgnoreFilter::new) + .map(Arc::new) + .collect(); Self { index, state: Arc::new(RwLock::new(UpdaterState::Idle)), - workspace_root, - ignore_filter, + workspace_roots, + ignore_filters, } } + fn should_ignore(&self, path: &Path) -> bool { + self.workspace_roots + .iter() + .zip(&self.ignore_filters) + .find(|(root, _)| path.starts_with(root)) + .map(|(_, filter)| filter.should_ignore(path)) + .unwrap_or(false) + } + /// Process a single file update fn process_file_update(&self, path: &Path) -> Result<()> { // Check if the file should be ignored - if self.ignore_filter.should_ignore(path) { + if self.should_ignore(path) { debug!("Ignoring file update for: {}", path.display()); return Ok(()); } @@ -90,8 +106,10 @@ impl IndexUpdater { // Create a new temporary index with the same parser backend let new_index = Arc::new(SymbolIndex::new(self.index.parser_backend())); - // Index the workspace into the new index - new_index.clone().index_workspace(&self.workspace_root)?; + // Index each workspace folder into the new index + for root in &self.workspace_roots { + new_index.clone().index_workspace(root)?; + } // Atomically swap the indices self.index.swap_index(&new_index); @@ -157,16 +175,16 @@ impl FileEventHandler for IndexUpdater { // Clone what we need for the async task let index = self.index.clone(); let state = self.state.clone(); - let workspace_root = self.workspace_root.clone(); - let ignore_filter = self.ignore_filter.clone(); + let workspace_roots = self.workspace_roots.clone(); + let ignore_filters = self.ignore_filters.clone(); // Use rayon's thread pool instead of spawning new threads rayon::spawn(move || { let updater = IndexUpdater { index, state, - workspace_root, - ignore_filter, + workspace_roots, + ignore_filters, }; updater.handle_event_internal(event); }); @@ -187,7 +205,7 @@ impl FileEventHandler for IndexUpdater { } // Check if the file should be ignored - let should_ignore = self.ignore_filter.should_ignore(path); + let should_ignore = self.should_ignore(path); if should_ignore { debug!("Ignoring watch for: {}", path.display()); @@ -197,7 +215,10 @@ impl FileEventHandler for IndexUpdater { } fn workspace_root(&self) -> &Path { - &self.workspace_root + self.workspace_roots + .first() + .map(PathBuf::as_path) + .unwrap_or_else(|| Path::new(".")) } } diff --git a/pylight/src/lsp/server.rs b/pylight/src/lsp/server.rs index cf5854c..5c4affc 100644 --- a/pylight/src/lsp/server.rs +++ b/pylight/src/lsp/server.rs @@ -16,11 +16,35 @@ pub struct LspServer { connection: Connection, index: Arc, search_engine: Arc, - workspace_root: Option, + workspace_roots: Vec, cancelled_requests: Arc>>, _file_watcher: Option, } +fn uri_to_path(uri: &lsp_types::Uri) -> Option { + url::Url::parse(uri.as_str()).ok()?.to_file_path().ok() +} + +fn workspace_roots_from_initialize_params(params: &InitializeParams) -> Vec { + if let Some(folders) = ¶ms.workspace_folders { + let roots: Vec = folders + .iter() + .filter_map(|folder| uri_to_path(&folder.uri)) + .collect(); + if !roots.is_empty() { + return roots; + } + } + + #[allow(deprecated)] + params + .root_uri + .as_ref() + .and_then(uri_to_path) + .into_iter() + .collect() +} + impl LspServer { pub fn new(parser_backend: ParserBackend) -> Result { let (connection, _io_threads) = Connection::stdio(); @@ -29,7 +53,7 @@ impl LspServer { connection, index: Arc::new(SymbolIndex::new(parser_backend)), search_engine: Arc::new(SearchEngine::new()), - workspace_root: None, + workspace_roots: Vec::new(), cancelled_requests: Arc::new(Mutex::new(HashSet::new())), _file_watcher: None, }) @@ -47,53 +71,53 @@ impl LspServer { .initialize(serde_json::to_value(server_capabilities).unwrap()) .map_err(|e| Error::Lsp(format!("Failed to initialize: {e}")))?; - // Extract workspace root + // Extract all workspace roots VS Code sends for multi-root workspaces. if let Ok(params) = serde_json::from_value::(initialization_params) { - #[allow(deprecated)] - if let Some(root_uri) = params.root_uri { - if let Ok(url) = url::Url::parse(root_uri.as_str()) { - if let Ok(path) = url.to_file_path() { - self.workspace_root = Some(path.clone()); - - // Start background indexing - let index = self.index.clone(); - let root = path.clone(); - let root_for_watcher = path.clone(); - - // Create the index updater and file watcher - let updater = Arc::new(IndexUpdater::new( - self.index.clone(), - root_for_watcher.clone(), - )); - let watcher_config = WatcherConfig::default(); - - match FileWatcher::new(watcher_config, updater) { - Ok(mut watcher) => { - // Start watching the workspace - if let Err(e) = watcher.watch(&root_for_watcher) { - tracing::error!("Failed to start file watcher: {}", e); - } else { - tracing::info!( - "File watcher started for workspace: {}", - root_for_watcher.display() - ); - self._file_watcher = Some(watcher); - } - } - Err(e) => { - tracing::error!("Failed to create file watcher: {}", e); - } - } + self.workspace_roots = workspace_roots_from_initialize_params(¶ms); + + if !self.workspace_roots.is_empty() { + let roots = self.workspace_roots.clone(); + let index = self.index.clone(); - thread::spawn(move || { - if let Err(e) = index.index_workspace(&root) { - tracing::error!("Failed to index workspace: {}", e); + // Create one updater/watcher and subscribe it to every workspace folder. + let updater = Arc::new(IndexUpdater::new_multi(self.index.clone(), roots.clone())); + let watcher_config = WatcherConfig::default(); + + match FileWatcher::new(watcher_config, updater) { + Ok(mut watcher) => { + let mut watching = false; + for root in &roots { + if let Err(e) = watcher.watch(root) { + tracing::error!( + "Failed to start file watcher for {}: {}", + root.display(), + e + ); } else { - tracing::info!("Initial workspace indexing completed"); + watching = true; + tracing::info!( + "File watcher started for workspace: {}", + root.display() + ); } - }); + } + if watching { + self._file_watcher = Some(watcher); + } + } + Err(e) => { + tracing::error!("Failed to create file watcher: {}", e); } } + + thread::spawn(move || { + for root in &roots { + if let Err(e) = index.clone().index_workspace(root) { + tracing::error!("Failed to index workspace {}: {}", root.display(), e); + } + } + tracing::info!("Initial workspace indexing completed"); + }); } } @@ -244,3 +268,44 @@ impl LspServer { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn initialize_params_use_all_workspace_folders() { + let params: InitializeParams = serde_json::from_value(json!({ + "processId": null, + "rootUri": "file:///top", + "capabilities": {}, + "workspaceFolders": [ + { "uri": "file:///repo-a", "name": "repo-a" }, + { "uri": "file:///repo-b", "name": "repo-b" } + ] + })) + .unwrap(); + + let roots = workspace_roots_from_initialize_params(¶ms); + assert_eq!( + roots, + vec![PathBuf::from("/repo-a"), PathBuf::from("/repo-b")] + ); + } + + #[test] + fn initialize_params_fall_back_to_root_uri() { + let params: InitializeParams = serde_json::from_value(json!({ + "processId": null, + "rootUri": "file:///top", + "capabilities": {} + })) + .unwrap(); + + assert_eq!( + workspace_roots_from_initialize_params(¶ms), + vec![PathBuf::from("/top")] + ); + } +} diff --git a/pylight/src/watcher.rs b/pylight/src/watcher.rs index 39165ef..d17a70d 100644 --- a/pylight/src/watcher.rs +++ b/pylight/src/watcher.rs @@ -1,6 +1,5 @@ //! File system watcher with debouncing support -use crate::file_filter::IgnoreFilter; use crate::Result; use crossbeam_channel::{self, Receiver, RecvTimeoutError, Sender}; use notify::{Event, EventKind, RecursiveMode, Watcher}; @@ -9,7 +8,7 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use std::thread; use std::time::{Duration, Instant}; -use tracing::{debug, info}; +use tracing::info; /// Configuration for the file watcher #[derive(Debug, Clone)] @@ -50,7 +49,6 @@ pub struct FileWatcher { _event_handler: Arc, _shutdown_tx: Sender<()>, _debouncer_handle: Option>, - _ignore_filter: Arc, } /// Trait for handling file system events @@ -74,11 +72,6 @@ impl FileWatcher { let (event_tx, event_rx) = crossbeam_channel::unbounded::(); let (shutdown_tx, shutdown_rx) = crossbeam_channel::bounded::<()>(1); - // Create the ignore filter - let ignore_filter = Arc::new(IgnoreFilter::new( - event_handler.workspace_root().to_path_buf(), - )); - // Create the notify watcher let watcher = notify::recommended_watcher(move |res: notify::Result| { if let Ok(event) = res { @@ -90,15 +83,8 @@ impl FileWatcher { // Spawn the debouncer thread let event_handler_clone = event_handler.clone(); let config_clone = config.clone(); - let ignore_filter_clone = ignore_filter.clone(); let handle = thread::spawn(move || { - Self::debouncer_thread( - config_clone, - event_handler_clone, - event_rx, - shutdown_rx, - ignore_filter_clone, - ); + Self::debouncer_thread(config_clone, event_handler_clone, event_rx, shutdown_rx); }); Ok(Self { @@ -107,7 +93,6 @@ impl FileWatcher { _event_handler: event_handler, _shutdown_tx: shutdown_tx, _debouncer_handle: Some(handle), - _ignore_filter: ignore_filter, }) } @@ -117,7 +102,6 @@ impl FileWatcher { event_handler: Arc, event_rx: Receiver, shutdown_rx: Receiver<()>, - ignore_filter: Arc, ) { let mut pending_events = HashSet::new(); let mut last_event_time = Instant::now(); @@ -148,12 +132,6 @@ impl FileWatcher { Ok(event) => { // Process the event for path in event.paths { - // Use ignore filter to check if we should process this path - if ignore_filter.should_ignore(&path) { - debug!("Ignoring event for: {}", path.display()); - continue; - } - match event.kind { EventKind::Create(_) | EventKind::Modify(_) => { if event_handler.should_watch(&path) { From c05a8c21712582ad6e7bc5358e7ea57b1c411b25 Mon Sep 17 00:00:00 2001 From: Ankit Chouhan Date: Wed, 24 Jun 2026 18:57:55 +0530 Subject: [PATCH 2/4] Add VS Code multi-root integration tests --- pydance/src/test/runIntegrationTest.ts | 13 +- pydance/src/test/suite/extension.test.ts | 115 ++++++++++++++---- .../src/testFixture/multi-root.code-workspace | 6 + pydance/src/testFixtureSecond/second.py | 11 ++ 4 files changed, 115 insertions(+), 30 deletions(-) create mode 100644 pydance/src/testFixture/multi-root.code-workspace create mode 100644 pydance/src/testFixtureSecond/second.py diff --git a/pydance/src/test/runIntegrationTest.ts b/pydance/src/test/runIntegrationTest.ts index 0708b35..e511712 100644 --- a/pydance/src/test/runIntegrationTest.ts +++ b/pydance/src/test/runIntegrationTest.ts @@ -1,3 +1,4 @@ +import * as fs from "fs"; import * as path from "path"; import { runTests } from "@vscode/test-electron"; @@ -9,8 +10,14 @@ async function main() { // The path to test runner for integration tests const extensionTestsPath = path.resolve(__dirname, "./suite/index"); - // The path to the test workspace - this ensures we have a proper workspace - const testWorkspace = path.resolve(__dirname, "../../src/testFixture"); + // The path to the test workspace - this ensures we have a proper multi-root workspace + const testWorkspace = path.resolve( + __dirname, + "../../src/testFixture/multi-root.code-workspace" + ); + + const userDataDir = path.join("/tmp", `pydance-vscode-test-${process.pid}`); + fs.rmSync(userDataDir, { recursive: true, force: true }); console.log("Running integration tests with workspace:", testWorkspace); @@ -18,7 +25,7 @@ async function main() { await runTests({ extensionDevelopmentPath, extensionTestsPath, - launchArgs: [testWorkspace], + launchArgs: [testWorkspace, "--user-data-dir", userDataDir], // Set environment variable to indicate integration test mode extensionTestsEnv: { INTEGRATION_TEST: "true", diff --git a/pydance/src/test/suite/extension.test.ts b/pydance/src/test/suite/extension.test.ts index 47b5f0f..25e926e 100644 --- a/pydance/src/test/suite/extension.test.ts +++ b/pydance/src/test/suite/extension.test.ts @@ -82,34 +82,11 @@ suite("Extension Test Suite", () => { }); suite("Integration Tests", () => { - test("Should provide workspace symbols with real language server", async function () { - // Skip if not running in integration test mode - if (process.env.INTEGRATION_TEST !== "true") { - console.log("Not in integration test mode, skipping"); - this.skip(); - } - - // First check if we have a workspace folder - const workspaceFolders = vscode.workspace.workspaceFolders; - if (!workspaceFolders || workspaceFolders.length === 0) { - console.log("No workspace folder found, skipping integration test"); - this.skip(); - } - - // Check if pylight binary exists - // eslint-disable-next-line @typescript-eslint/no-var-requires - const fs = require("fs"); - // eslint-disable-next-line @typescript-eslint/no-var-requires - const path = require("path"); - const extensionPath = - vscode.extensions.getExtension("ToughType.pydance")!.extensionPath; - const pylightPath = path.join(extensionPath, "pylight"); - - if (!fs.existsSync(pylightPath)) { - console.log("pylight binary not found, skipping integration test"); - this.skip(); - } + setup(function () { + skipUnlessIntegrationReady(this); + }); + test("Should provide workspace symbols with real language server", async function () { await testWorkspaceSymbols(docUri, [ new vscode.SymbolInformation( "TestClass", @@ -162,9 +139,93 @@ suite("Extension Test Suite", () => { // Note: TEST_CONSTANT is not returned by the server when searching for "test" ]); }); + + test("Should index all folders in a multi-root workspace", async function () { + const repoTwoFolder = vscode.workspace.workspaceFolders?.find( + (folder) => folder.name === "testFixtureSecond" + ); + assert.ok(repoTwoFolder, "Expected second workspace folder to be open"); + + await activate(docUri); + const symbols = await waitForWorkspaceSymbols("repo_two", 2); + const names = symbols.map((symbol) => symbol.name); + + assert.ok( + names.includes("repo_two_function"), + "repo_two_function should be indexed" + ); + assert.ok( + names.includes("repo_two_helper"), + "repo_two_helper should be indexed" + ); + assert.ok( + symbols.every((symbol) => + symbol.location.uri.fsPath.startsWith(repoTwoFolder!.uri.fsPath) + ), + "repo_two results should come from the second workspace folder" + ); + }); + + test("Should expose a restart command that reindexes", async function () { + await activate(docUri); + await vscode.commands.executeCommand("pydance.restartServer"); + + const symbols = await waitForWorkspaceSymbols("repo_two", 2); + assert.ok( + symbols.some((symbol) => symbol.name === "repo_two_function"), + "Expected repo_two_function after restart" + ); + }); }); }); +function skipUnlessIntegrationReady(context: Mocha.Context) { + if (process.env.INTEGRATION_TEST !== "true") { + console.log("Not in integration test mode, skipping"); + context.skip(); + } + + const workspaceFolders = vscode.workspace.workspaceFolders; + if (!workspaceFolders || workspaceFolders.length === 0) { + console.log("No workspace folder found, skipping integration test"); + context.skip(); + } + + // Check if pylight binary exists + // eslint-disable-next-line @typescript-eslint/no-var-requires + const fs = require("fs"); + // eslint-disable-next-line @typescript-eslint/no-var-requires + const path = require("path"); + const extensionPath = + vscode.extensions.getExtension("ToughType.pydance")!.extensionPath; + const pylightPath = path.join(extensionPath, "pylight"); + + if (!fs.existsSync(pylightPath)) { + console.log("pylight binary not found, skipping integration test"); + context.skip(); + } +} + +async function waitForWorkspaceSymbols(query: string, minCount: number) { + const deadline = Date.now() + 10_000; + let symbols: vscode.SymbolInformation[] = []; + + while (Date.now() < deadline) { + symbols = await vscode.commands.executeCommand( + "vscode.executeWorkspaceSymbolProvider", + query + ); + if (symbols.length >= minCount) { + return symbols; + } + await new Promise((resolve) => setTimeout(resolve, 250)); + } + + assert.fail( + `Expected at least ${minCount} symbols for ${query}, found ${symbols.length}` + ); +} + async function testWorkspaceSymbols( docUri: vscode.Uri, expectedSymbols: vscode.SymbolInformation[] diff --git a/pydance/src/testFixture/multi-root.code-workspace b/pydance/src/testFixture/multi-root.code-workspace new file mode 100644 index 0000000..c54afaf --- /dev/null +++ b/pydance/src/testFixture/multi-root.code-workspace @@ -0,0 +1,6 @@ +{ + "folders": [ + { "path": "." }, + { "path": "../testFixtureSecond" } + ] +} diff --git a/pydance/src/testFixtureSecond/second.py b/pydance/src/testFixtureSecond/second.py new file mode 100644 index 0000000..b0e22fd --- /dev/null +++ b/pydance/src/testFixtureSecond/second.py @@ -0,0 +1,11 @@ +class RepoTwoClass: + def repo_two_method(self): + pass + + +def repo_two_function(): + return "repo two" + + +def repo_two_helper(): + return "helper" From 71b97846ea7d358c893a455133007b9de3fe407c Mon Sep 17 00:00:00 2001 From: Ankit Chouhan Date: Wed, 24 Jun 2026 19:32:31 +0530 Subject: [PATCH 3/4] Move multi-root fixture outside workspace folders --- pydance/src/test/runIntegrationTest.ts | 2 +- .../{testFixture => testWorkspaces}/multi-root.code-workspace | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename pydance/src/{testFixture => testWorkspaces}/multi-root.code-workspace (64%) diff --git a/pydance/src/test/runIntegrationTest.ts b/pydance/src/test/runIntegrationTest.ts index e511712..a19da9d 100644 --- a/pydance/src/test/runIntegrationTest.ts +++ b/pydance/src/test/runIntegrationTest.ts @@ -13,7 +13,7 @@ async function main() { // The path to the test workspace - this ensures we have a proper multi-root workspace const testWorkspace = path.resolve( __dirname, - "../../src/testFixture/multi-root.code-workspace" + "../../src/testWorkspaces/multi-root.code-workspace" ); const userDataDir = path.join("/tmp", `pydance-vscode-test-${process.pid}`); diff --git a/pydance/src/testFixture/multi-root.code-workspace b/pydance/src/testWorkspaces/multi-root.code-workspace similarity index 64% rename from pydance/src/testFixture/multi-root.code-workspace rename to pydance/src/testWorkspaces/multi-root.code-workspace index c54afaf..393134e 100644 --- a/pydance/src/testFixture/multi-root.code-workspace +++ b/pydance/src/testWorkspaces/multi-root.code-workspace @@ -1,6 +1,6 @@ { "folders": [ - { "path": "." }, + { "path": "../testFixture" }, { "path": "../testFixtureSecond" } ] } From f6322d27cd308c9dd0c188e95ea97f5f17faf53c Mon Sep 17 00:00:00 2001 From: Ankit Chouhan Date: Wed, 24 Jun 2026 19:51:35 +0530 Subject: [PATCH 4/4] Keep single-root integration runner --- pydance/package.json | 1 + pydance/src/test/runIntegrationTest.ts | 7 ++-- .../src/test/runMultiRootIntegrationTest.ts | 32 +++++++++++++++++++ pydance/src/test/suite/extension.test.ts | 21 ++++++++---- 4 files changed, 49 insertions(+), 12 deletions(-) create mode 100644 pydance/src/test/runMultiRootIntegrationTest.ts diff --git a/pydance/package.json b/pydance/package.json index f64ee28..1e248d6 100644 --- a/pydance/package.json +++ b/pydance/package.json @@ -68,6 +68,7 @@ "pretest": "npm run compile:tests", "test": "node ./out/test/runTest.js", "test:integration": "npm run compile:tests && node ./out/test/runIntegrationTest.js", + "test:integration:multi-root": "npm run compile:tests && node ./out/test/runMultiRootIntegrationTest.js", "lint": "eslint src --ext ts", "lint:fix": "eslint src --ext ts --fix", "format": "prettier --write \"src/**/*.ts\"", diff --git a/pydance/src/test/runIntegrationTest.ts b/pydance/src/test/runIntegrationTest.ts index a19da9d..7b8eeb4 100644 --- a/pydance/src/test/runIntegrationTest.ts +++ b/pydance/src/test/runIntegrationTest.ts @@ -10,11 +10,8 @@ async function main() { // The path to test runner for integration tests const extensionTestsPath = path.resolve(__dirname, "./suite/index"); - // The path to the test workspace - this ensures we have a proper multi-root workspace - const testWorkspace = path.resolve( - __dirname, - "../../src/testWorkspaces/multi-root.code-workspace" - ); + // The path to the test workspace - this ensures we have a proper workspace + const testWorkspace = path.resolve(__dirname, "../../src/testFixture"); const userDataDir = path.join("/tmp", `pydance-vscode-test-${process.pid}`); fs.rmSync(userDataDir, { recursive: true, force: true }); diff --git a/pydance/src/test/runMultiRootIntegrationTest.ts b/pydance/src/test/runMultiRootIntegrationTest.ts new file mode 100644 index 0000000..79df80c --- /dev/null +++ b/pydance/src/test/runMultiRootIntegrationTest.ts @@ -0,0 +1,32 @@ +import * as fs from "fs"; +import * as path from "path"; +import { runTests } from "@vscode/test-electron"; + +async function main() { + try { + const extensionDevelopmentPath = path.resolve(__dirname, "../../"); + const extensionTestsPath = path.resolve(__dirname, "./suite/index"); + const testWorkspace = path.resolve( + __dirname, + "../../src/testWorkspaces/multi-root.code-workspace" + ); + const userDataDir = path.join("/tmp", `pydance-vscode-test-${process.pid}`); + fs.rmSync(userDataDir, { recursive: true, force: true }); + + console.log("Running multi-root integration tests with workspace:", testWorkspace); + + await runTests({ + extensionDevelopmentPath, + extensionTestsPath, + launchArgs: [testWorkspace, "--user-data-dir", userDataDir], + extensionTestsEnv: { + INTEGRATION_TEST: "true", + }, + }); + } catch (err) { + console.error("Failed to run multi-root integration tests"); + process.exit(1); + } +} + +main(); diff --git a/pydance/src/test/suite/extension.test.ts b/pydance/src/test/suite/extension.test.ts index 25e926e..60b09fc 100644 --- a/pydance/src/test/suite/extension.test.ts +++ b/pydance/src/test/suite/extension.test.ts @@ -141,10 +141,11 @@ suite("Extension Test Suite", () => { }); test("Should index all folders in a multi-root workspace", async function () { - const repoTwoFolder = vscode.workspace.workspaceFolders?.find( - (folder) => folder.name === "testFixtureSecond" - ); - assert.ok(repoTwoFolder, "Expected second workspace folder to be open"); + const repoTwoFolder = secondWorkspaceFolder(); + if (!repoTwoFolder) { + console.log("No second workspace folder found, skipping multi-root test"); + this.skip(); + } await activate(docUri); const symbols = await waitForWorkspaceSymbols("repo_two", 2); @@ -170,15 +171,21 @@ suite("Extension Test Suite", () => { await activate(docUri); await vscode.commands.executeCommand("pydance.restartServer"); - const symbols = await waitForWorkspaceSymbols("repo_two", 2); + const symbols = await waitForWorkspaceSymbols("test", 4); assert.ok( - symbols.some((symbol) => symbol.name === "repo_two_function"), - "Expected repo_two_function after restart" + symbols.some((symbol) => symbol.name === "test_function"), + "Expected test_function after restart" ); }); }); }); +function secondWorkspaceFolder() { + return vscode.workspace.workspaceFolders?.find( + (folder) => folder.name === "testFixtureSecond" + ); +} + function skipUnlessIntegrationReady(context: Mocha.Context) { if (process.env.INTEGRATION_TEST !== "true") { console.log("Not in integration test mode, skipping");