Skip to content

feat: introduce multi-project workspace support and schema isolation#523

Open
harshanacz wants to merge 13 commits intowso2:mi-ext-alpha-releasefrom
harshanacz:refactor/multi-workspace-support
Open

feat: introduce multi-project workspace support and schema isolation#523
harshanacz wants to merge 13 commits intowso2:mi-ext-alpha-releasefrom
harshanacz:refactor/multi-workspace-support

Conversation

@harshanacz
Copy link
Copy Markdown

@harshanacz harshanacz commented Apr 10, 2026

Implementing Multi-Project Workspace Support for Current Language Server

The current WSO2 Micro Integrator (MI) Language Server typically serves each MI project with a separate language server instance when multiple projects exist in a workspace. This limits scalability, increases resource overhead, and breaks true multi-folder workspace experiences.

Overview

1. Core XML Validation & Schema Isolation (Completed)

2. Eliminating Global State, Singletons & Context-Aware

3. Language Client (VS Code Extension) Integration


Stage 1 Completed: LemMinX File Associations for Workspace Schema Validation

Technical Overview

Introduced File Associations to replace the legacy namespace-based catalog implementation. Instead of relying on
rigid catalogs, this approach maps specific file path patterns (e.g., glob matches for a project folder) to a specific target schema path. This enables different schemas (e.g., MI 4.3.0 and 4.4.0) to be applied to different projects simultaneously without conflict in a single LemMinX instance.

Changelog & Implementation Details

1. Utils.java (org.eclipse.lemminx/customservice/synapse/utils/Utils.java)

  • Implementation of updateSynapseFileAssociationSettings Method:
    • Previously, the system relied on updateSynapseCatalogSettings.
    • Added this new functionality to support File Association within the language server.

2. XMLLanguageServer.java (org.eclipse.lemminx/XMLLanguageServer.java)

  • Initialization Mode Selection: The language server now dynamically determines the validation method during the initialize step by reading the useAssociationSettings flag from initializationOptions:
    • If true (Default): The server invokes Utils.updateSynapseFileAssociationSettings, utilizing the new core standard based on file associations.
    • If false: The server routes to Utils.updateSynapseCatalogSettings to process legacy catalog-based configurations.
    • If null/Undefined: The server defaults to the file association method (true).
  • Temporary Bridge:
    • Because SynapseLanguageService (Stage 2) is not yet fully isolated for multi-root awareness, a temporary bridge was established.
    • It sets the default Path to the first project in the collection: synapseLanguageService.setSynapseXSDPath(workspaceSchemas.values().iterator().next());

3. XMLWorkspaceService.java (org.eclipse.lemminx/XMLWorkspaceService.java)

  • Enhanced didChangeWorkspaceFolders Logic:
    • Updated to capture dynamic workspace events correctly.
    • If a user adds a new project to the workspace after server initialization, it accurately intercepts the action and generates/applies XSD schemas for the new folder.

4. CleanMultiRootValidationTest.java (.../extensions/contentmodel/CleanMultiRootValidationTest.java)

Established three comprehensive multi-root tests demonstrating core functionality:

  1. Multi-Root Isolation: Verifies that a single Language Server successfully provides isolated validations to two independent projects governed by distinct XSD files.
  2. Dynamic Connector Generation Test: Ensures that dynamically generated connector schemas are instantly picked up by the validation engine logic, operating independently of server restarts.
  3. Dynamic Workspace Handling: Asserts that when an entirely new project is dynamically appended to the workspace context at runtime, the language server successfully triggers its standard MI validations for the newly tracked space.

How to Test

Testing via VS Code Extension

To verify the multi-root support directly within the VS Code environment:

  1. Build the Project: Run the following command from the root directory to generate the server JAR:
    mvn clean install -DskipTests
  2. Update Extension Binary: Navigate to the target/ directory, locate the newly built JAR, and copy it into the ls/ folder of your VS Code extension installation (replacing the existing JAR).
  3. Configure Settings Toggle: When testing via the extension, you can toggle between the old and new logic by passing useAssociationSettings within your Language Server's initializationOptions.
    • Default Behavior: If the useAssociationSettings field is not mentioned (omitted), the server will automatically default to true and work with the new File Association logic.
    • If explicitly set to true, the language server continues to use the multi-root fileAssociation logic.
    • If explicitly set to false, the language server natively falls back to utilizing the legacy single-project catalog settings extraction logic.

Standalone Test Execution

For automated testing without the IDE:

  1. Navigate to Root:
    cd mi-language-server
  2. Run Targeted Tests: Execute the specialized multi-root validation suite using Maven:
    mvn -Dtest=CleanMultiRootValidationTest test

Next Steps

Eliminating Global State, Singletons & Context-Aware

Transition legacy singletons (e.g., ConnectorHolder, SynapseLanguageService, Mediator Handlers ....) to be
resolved per project context instead of a global state.

  • Introduce ProjectContext and WorkspaceManager classes to manage memory scoped to individual projects.

    • Reference Files: multi-workspace-support/resources/ProjectContext.java, multi-workspace-support/resources/WorkspaceManager.java
  • Isolate Language Server features (Auto-Complete, Go-To-Definition) per workspace.

Language Client (VS Code Extension) Integration

Update the frontend VS Code Extension to natively support the multi-project backend API configurations and event hooks.

@harshanacz harshanacz requested a review from rosensilva as a code owner April 10, 2026 11:10
Comment on lines +219 to +223
* @param miServerPath absolute path to the local MI server installation
* @param languageClient the language-client proxy for sending notifications
* back to the IDE
* @throws Exception if any step in the initialization pipeline fails
*/
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Log Improvement Suggestion No: 1

Suggested change
* @param miServerPath absolute path to the local MI server installation
* @param languageClient the language-client proxy for sending notifications
* back to the IDE
* @throws Exception if any step in the initialization pipeline fails
*/
public void initProject(String miServerPath, SynapseLanguageClientAPI languageClient) throws Exception {
log.log(Level.INFO, "Initializing ProjectContext for: " + projectUri);
// 1. Initialize inbound connector metadata.

Comment on lines +250 to +251
this.resourceFinder = ResourceFinderFactory.getResourceFinder(isLegacyProject);
resourceFinder.loadDependentResources(projectUri);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Log Improvement Suggestion No: 2

Suggested change
this.resourceFinder = ResourceFinderFactory.getResourceFinder(isLegacyProject);
resourceFinder.loadDependentResources(projectUri);
} catch (Exception e) {
log.log(Level.ERROR, "Failed to initialize ProjectContext for: " + projectUri + ". Error: " + e.getMessage());
throw e;
}
this.initialized = true;

Comment on lines +99 to +100
* @param projectUri the normalized root URI of the project to remove
* @return the removed {@link ProjectContext}, or {@code null} if not found
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Log Improvement Suggestion No: 3

Suggested change
* @param projectUri the normalized root URI of the project to remove
* @return the removed {@link ProjectContext}, or {@code null} if not found
public ProjectContext removeProject(String projectUri) {
if (projectUri == null) {
log.log(Level.WARNING, "removeProject called with null projectUri — ignoring.");
return null;
}

Comment on lines +150 to +154
* @param documentUri the normalized URI of the document being processed
* @return the best-matching {@link ProjectContext}, or {@code null} if no
* registered project contains the document
*/
public ProjectContext getProjectForDocument(String documentUri) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Log Improvement Suggestion No: 4

Suggested change
* @param documentUri the normalized URI of the document being processed
* @return the best-matching {@link ProjectContext}, or {@code null} if no
* registered project contains the document
*/
public ProjectContext getProjectForDocument(String documentUri) {
public ProjectContext getProjectForDocument(String documentUri) {
if (documentUri == null) {
log.log(Level.WARNING, "getProjectForDocument called with null documentUri — returning null.");
return null;
}

Comment on lines +136 to +141
if (!useAssociationSettings) {
Path synapseSchemaPath = Utils.updateSynapseCatalogSettings(params);
if (synapseSchemaPath != null) {
synapseLanguageService.setSynapseXSDPath(synapseSchemaPath);
}
} else {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Log Improvement Suggestion No: 5

Suggested change
if (!useAssociationSettings) {
Path synapseSchemaPath = Utils.updateSynapseCatalogSettings(params);
if (synapseSchemaPath != null) {
synapseLanguageService.setSynapseXSDPath(synapseSchemaPath);
}
} else {
try {
Path synapseSchemaPath = Utils.updateSynapseCatalogSettings(params);
if (synapseSchemaPath != null) {
LOGGER.info("Synapse schema path set to: " + synapseSchemaPath);
synapseLanguageService.setSynapseXSDPath(synapseSchemaPath);
}

Comment on lines +141 to +146
} else {
workspaceSchemas = Utils.updateSynapseFileAssociationSettings(params);
if (!workspaceSchemas.isEmpty()) {
synapseLanguageService.setSynapseXSDPath(workspaceSchemas.values().iterator().next());
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Log Improvement Suggestion No: 6

Suggested change
} else {
workspaceSchemas = Utils.updateSynapseFileAssociationSettings(params);
if (!workspaceSchemas.isEmpty()) {
synapseLanguageService.setSynapseXSDPath(workspaceSchemas.values().iterator().next());
}
}
} else {
workspaceSchemas = Utils.updateSynapseFileAssociationSettings(params);
if (!workspaceSchemas.isEmpty()) {
LOGGER.info("Loaded " + workspaceSchemas.size() + " workspace schemas");
synapseLanguageService.setSynapseXSDPath(workspaceSchemas.values().iterator().next());
}

Comment on lines +88 to +93
boolean hasSchemaChanges = false;
if (params.getEvent().getRemoved() != null) {
for (org.eclipse.lsp4j.WorkspaceFolder folder : params.getEvent().getRemoved()) {
xmlLanguageServer.removeWorkspaceSchema(folder.getUri());
hasSchemaChanges = true;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Log Improvement Suggestion No: 7

Suggested change
boolean hasSchemaChanges = false;
if (params.getEvent().getRemoved() != null) {
for (org.eclipse.lsp4j.WorkspaceFolder folder : params.getEvent().getRemoved()) {
xmlLanguageServer.removeWorkspaceSchema(folder.getUri());
hasSchemaChanges = true;
}
boolean hasSchemaChanges = false;
if (params.getEvent().getRemoved() != null) {
for (org.eclipse.lsp4j.WorkspaceFolder folder : params.getEvent().getRemoved()) {
if (log.isDebugEnabled()) {
log.debug("Removing workspace folder: " + folder.getUri());
}
xmlLanguageServer.removeWorkspaceSchema(folder.getUri());
hasSchemaChanges = true;

Comment on lines +100 to +103
hasSchemaChanges = true;
} catch (Exception e) {
// Ignore safely
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Log Improvement Suggestion No: 8

Suggested change
hasSchemaChanges = true;
} catch (Exception e) {
// Ignore safely
}
hasSchemaChanges = true;
} catch (Exception e) {
log.error("Failed to copy XSD files for workspace folder: " + folder.getUri() + ". Error: " + e.getMessage());
}

Comment on lines +1076 to +1077
public static Map<String, Path> updateSynapseFileAssociationSettings(InitializeParams params)
throws IOException, URISyntaxException {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Log Improvement Suggestion No: 9

Suggested change
public static Map<String, Path> updateSynapseFileAssociationSettings(InitializeParams params)
throws IOException, URISyntaxException {
public static Map<String, Path> updateSynapseFileAssociationSettings(InitializeParams params)
throws IOException, URISyntaxException {
LOGGER.info("Updating Synapse file association settings");

Comment on lines +1124 to +1125
patternBase = Paths.get(new URI(folderUri)).toString().replace("\\", "/");
} catch (Exception e) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Log Improvement Suggestion No: 10

Suggested change
patternBase = Paths.get(new URI(folderUri)).toString().replace("\\", "/");
} catch (Exception e) {
} catch (Exception e) {
LOGGER.warn("Failed to convert folder URI to filesystem path: " + folderUri);
patternBase = folderUri.replace("\\", "/");

Copy link
Copy Markdown

@wso2-engineering wso2-engineering bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI Agent Log Improvement Checklist

⚠️ Warning: AI-Generated Review Comments

  • The log-related comments and suggestions in this review were generated by an AI tool to assist with identifying potential improvements. Purpose of reviewing the code for log improvements is to improve the troubleshooting capabilities of our products.
  • Please make sure to manually review and validate all suggestions before applying any changes. Not every code suggestion would make sense or add value to our purpose. Therefore, you have the freedom to decide which of the suggestions are helpful.

✅ Before merging this pull request:

  • Review all AI-generated comments for accuracy and relevance.
  • Complete and verify the table below. We need your feedback to measure the accuracy of these suggestions and the value they add. If you are rejecting a certain code suggestion, please mention the reason briefly in the suggestion for us to capture it.
Comment Accepted (Y/N) Reason
#### Log Improvement Suggestion No: 1
#### Log Improvement Suggestion No: 2
#### Log Improvement Suggestion No: 3
#### Log Improvement Suggestion No: 4
#### Log Improvement Suggestion No: 5
#### Log Improvement Suggestion No: 6
#### Log Improvement Suggestion No: 7
#### Log Improvement Suggestion No: 8
#### Log Improvement Suggestion No: 9
#### Log Improvement Suggestion No: 10

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 10, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7a05de1b-97c7-479e-8d4c-7e0f8cb940ae

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@harshanacz
Copy link
Copy Markdown
Author

Comment Accepted (Y/N) Reason
#### Log Improvement Suggestion No: 1 Y
#### Log Improvement Suggestion No: 2 Y
#### Log Improvement Suggestion No: 3 Y
#### Log Improvement Suggestion No: 4 Y
#### Log Improvement Suggestion No: 5 Y
#### Log Improvement Suggestion No: 6 Y
#### Log Improvement Suggestion No: 7 Y
#### Log Improvement Suggestion No: 8 Y
#### Log Improvement Suggestion No: 9 Y
#### Log Improvement Suggestion No: 10 Y

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant