Skip to content

Comments

Implement custom LS API for CodeMap generation in BI Copilot#649

Draft
yasithrashan wants to merge 33 commits intoballerina-platform:mainfrom
yasithrashan:feature/custom-ls-api
Draft

Implement custom LS API for CodeMap generation in BI Copilot#649
yasithrashan wants to merge 33 commits intoballerina-platform:mainfrom
yasithrashan:feature/custom-ls-api

Conversation

@yasithrashan
Copy link

@yasithrashan yasithrashan commented Jan 21, 2026

Purpose

Related to: wso2/product-ballerina-integrator#2192

Ballerina Copilot currently sends the full project code to the LLM, which causes high token usage and slow responses.
To fix this, we generate a CodeMap of the project using a custom Language Server (LS) API and send this CodeMap to the LLM so it can understand and navigate the codebase without needing the full source.

Goals

  • Extract structured code artifacts from the project

Approach

  • Create a custom Ballerina Language Server (LS) API to generate a CodeMap for the project.
  • Use the API to extract artifacts from the entire project (services, functions, types, variables, listeners, etc.).

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements a custom Language Server API to generate a CodeMap for Ballerina Copilot, reducing token usage by sending structured code artifacts instead of full source code to the LLM.

Changes:

  • Introduced CodeMap generation API with request/response models and service endpoint
  • Implemented AST transformation to extract structured artifacts (services, functions, types, listeners, connections, etc.)
  • Added comprehensive test coverage with multiple Ballerina project scenarios

Reviewed changes

Copilot reviewed 57 out of 57 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
CodeMapArtifact.java Data model for code artifacts with builder pattern
CodeMapFile.java Record representing a file with its artifacts
CodeMapGenerator.java Main generator logic iterating through project modules
CodeMapNodeTransformer.java AST visitor extracting artifacts from syntax nodes
CodeMapRequest.java Request model with project path
CodeMapResponse.java Response model with extracted files
DesignModelGeneratorService.java Added codeMap endpoint to existing service
module-info.java Exported codemap package
CodeMapGeneratorTest.java Test class with data provider pattern
testng.xml Added test class to test suite
codemap/source/* Test Ballerina projects for various scenarios
codemap/config/* Expected JSON output for test validation

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

import java.util.List;
import java.util.Map;

public record CodeMapArtifact(String name, String type, LineRange lineRange, List<String> modifiers,

Choose a reason for hiding this comment

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

Children can be a optional field, give a default for that

import java.util.List;
import java.util.Map;

public record CodeMapArtifact(String name, String type, LineRange lineRange, List<String> modifiers,
Copy link

@SasinduDilshara SasinduDilshara Jan 21, 2026

Choose a reason for hiding this comment

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

Please give some samples for modifiers, propertirs, children. What are they?

import java.util.List;
import java.util.Map;

public record CodeMapArtifact(String name, String type, LineRange lineRange, List<String> modifiers,

Choose a reason for hiding this comment

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

Where is the filename capture in here?

Copy link
Author

Choose a reason for hiding this comment

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

The filename is not captured directly. In the CodeMapGenerator.java file, within the
generateCodeMap function, we get the file name using:

String fileName = document.name();

Then, in the same function, after extracting the artifacts from a single file, we wrap
everything into a CodeMapFile

CodeMapFile codeMapFile = new CodeMapFile(fileName, relativeFilePath, artifacts);

@yasithrashan yasithrashan force-pushed the feature/custom-ls-api branch from 254bb0e to 2b2b5bc Compare January 23, 2026 09:45
import java.util.Collections;
import java.util.List;

public record CodeMapFile(String fileName, String relativeFilePath, List<CodeMapArtifact> artifacts) {
Copy link
Contributor

@nipunayf nipunayf Jan 21, 2026

Choose a reason for hiding this comment

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

Missing java doc.

Add java docs for all the public classes.

Choose a reason for hiding this comment

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

@yasithrashan This is mandatory.
All public constructs should have Javadocs while explaining the main functioanlity and inputs and outputs

And also please mark public only the necessary ones

Copy link
Author

Choose a reason for hiding this comment

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

Ack

@@ -0,0 +1,68 @@
/*
* Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com)
* Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com)

Apply this to other applicable areas.

* Tracks changed files per project for incremental code map generation.
* This singleton maintains a thread-safe record of file changes between API calls.
*
* @since 1.0.0
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* @since 1.0.0
* @since 1.6.0

Apply this to other applicable areas.

Copy link
Author

Choose a reason for hiding this comment

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

Ack

Comment on lines 46 to 49
if (instance == null) {
instance = new ChangedFilesTracker();
}
return instance;
Copy link
Contributor

Choose a reason for hiding this comment

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

Making the method synchronized may be overkill, since after the first call all subsequent calls are read-only. Use the holder method for handling the Singleton.

Copy link
Author

Choose a reason for hiding this comment

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

Ack


// Get and clear the list of changed files for a project.
public List<String> getAndClearChangedFiles(String projectKey) {
Set<String> files = changedFilesMap.remove(projectKey);
Copy link
Contributor

Choose a reason for hiding this comment

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

We should implement a rollback mechanism for this instruction. If the LS fails to respond, the state becomes desynchronized for the extension because files are being removed prematurely. Given the low likelihood of this error, adding a note is sufficient for now.

Copy link
Author

Choose a reason for hiding this comment

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

Ack

extractDocumentation(functionDefinitionNode.metadata()).ifPresent(functionBuilder::documentation);
extractComments(functionDefinitionNode).ifPresent(functionBuilder::comment);

functionBuilder.type("FUNCTION");
Copy link
Contributor

Choose a reason for hiding this comment

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

Extract the strings used to define the JSON fields to the class level.

Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need these changes for this PR?

Copy link
Author

Choose a reason for hiding this comment

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

Yes


if (!files.equals(testConfig.output())) {
TestConfig updatedConfig = new TestConfig(testConfig.description(), testConfig.source(), files);
updateConfig(configJsonPath, updatedConfig);
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
updateConfig(configJsonPath, updatedConfig);
// updateConfig(configJsonPath, updatedConfig);

Copy link
Author

Choose a reason for hiding this comment

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

Ack

*
* @since 1.0.0
*/
public class PublishCodeMapSubscriberTest extends AbstractLSTest {
Copy link
Contributor

Choose a reason for hiding this comment

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

Need to cover following scenarios if not included:

  1. Multiple File Accumulation - Trigger onEvent for two different files within the same project sequentially.
  2. Consecutive Events for Same File - Trigger onEvent multiple times for the same file.
  3. Same File in Different Modules: Trigger events for both types.bal and modules/mod1/types.bal.
  4. State Clearing - Verify that retrieving the changes "consumes" them, ensuring subsequent calls don't return stale data.
  5. Project Isolation: Multiple Project - Verify that changes in "Project A" do not leak into "Project B".

Copy link
Author

Choose a reason for hiding this comment

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

Ack

*
* @since 1.0.0
*/
public class CodeMapGeneratorTest extends AbstractLSTest {
Copy link
Contributor

Choose a reason for hiding this comment

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

Add a single test case with a Ballerina project as the source. Keep the code minimal, since the goal is only to verify that the API works for Ballerina projects, not to validate different construct types.

Copy link
Author

Choose a reason for hiding this comment

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

Ack


public static synchronized ChangedFilesTracker getInstance() {
if (instance == null) {
instance = new ChangedFilesTracker();
Copy link

@SasinduDilshara SasinduDilshara Jan 26, 2026

Choose a reason for hiding this comment

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

public static ChangedFilesTracker getInstance() {
        // First check (no locking)
        if (instance == null) {
            // Only synchronize if it looks like we need to create it
            synchronized (ChangedFilesTracker.class) {
                // Second check (inside the lock)
                if (instance == null) {
                    instance = new ChangedFilesTracker();
                }
            }
        }
        return instance;
    }

Copy link
Author

Choose a reason for hiding this comment

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

Ack

public List<String> getAndClearChangedFiles(String projectKey) {
Set<String> files = changedFilesMap.remove(projectKey);
if (files == null || files.isEmpty()) {
return Collections.emptyList();

Choose a reason for hiding this comment

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

Lets add a log for this

Copy link
Author

Choose a reason for hiding this comment

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

Ack


public Builder modifiers(List<String> modifiers) {
if (!modifiers.isEmpty()) {
this.properties.put("modifiers", new ArrayList<>(modifiers));

Choose a reason for hiding this comment

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

This kind of literals should be defines as constants

Copy link
Author

Choose a reason for hiding this comment

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

Ack

}

public Builder config(String config) {
return addProperty("config", config);

Choose a reason for hiding this comment

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

Is the config is a simple element?

ModuleInfo moduleInfo = createModuleInfo(module.descriptor());

// Iterate through each document in the module
for (var documentId : module.documentIds()) {

Choose a reason for hiding this comment

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

This may not contains resources

Choose a reason for hiding this comment

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

In here what I mean by resources is,
In a Ballerina project there can be a folder called resources.

It may contains images and other necessaru resources

Basically its not a bal file, But images and other resources necessaru for the code


for (var moduleId : currentPackage.moduleIds()) {
Module module = currentPackage.module(moduleId);
SemanticModel semanticModel =

Choose a reason for hiding this comment

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

Handle scearios where the semanticModel = null

PackageUtil.getCompilation(currentPackage).getSemanticModel(moduleId);
ModuleInfo moduleInfo = createModuleInfo(module.descriptor());

for (var documentId : module.documentIds()) {

Choose a reason for hiding this comment

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

Add a variable isTest = true/false, Depends on tat add test files also into here. It will be helpful when copilot generating tests

private final ModuleInfo moduleInfo;
private final boolean extractComments;

private static final String AUTOMATION_FUNCTION_NAME = "automation";

Choose a reason for hiding this comment

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

Please check, there is no syntax called automation

Copy link
Author

Choose a reason for hiding this comment

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

Ack

// Build full import name
String fullImportName = orgName.isEmpty() ? moduleName : orgName + "/" + moduleName;
if (alias.isPresent()) {
fullImportName += " as " + alias.get();

Choose a reason for hiding this comment

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

Add this terms as constnats.

Copy link
Author

Choose a reason for hiding this comment

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

Ack

} else if (firstExpression != null) {
return firstExpression.toSourceCode().strip();
} else {
return "";

Choose a reason for hiding this comment

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

There is no meaning of empty string as service name

Choose a reason for hiding this comment

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

Use Optional.empty

Copy link
Author

Choose a reason for hiding this comment

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

Ack

return Optional.of(classSymbol);
}
} catch (Throwable e) {
// Ignore

Choose a reason for hiding this comment

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

Add a log

.filter(doc -> !doc.isEmpty());
}

private Optional<String> extractComments(Node node) {

Choose a reason for hiding this comment

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

Suggested change
private Optional<String> extractComments(Node node) {
private Optional<String> extractInlineComments(Node node) {

Copy link
Author

Choose a reason for hiding this comment

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

Ack

.getAndClearChangedFiles(projectKey);

if (changedFiles.isEmpty()) {
// No changes tracked, return empty response

Choose a reason for hiding this comment

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

Add logs

import ballerina/tcp;

// HTTP connection
final http:Client httpConnection = check new ("https://api.example.com", {

Choose a reason for hiding this comment

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

Add test for Url as varaibales as well

@coderabbitai
Copy link

coderabbitai bot commented Feb 9, 2026

Important

Review skipped

Draft detected.

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.

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
  • Post copyable unit tests in a comment

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

@yasithrashan yasithrashan force-pushed the feature/custom-ls-api branch from c486070 to e760d4c Compare February 9, 2026 05:43
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.

3 participants