Skip to content

Commit 7975b4c

Browse files
authored
Merge pull request #964 from xlight05/fix-ai-workspaces
Fix Add to integration in Copilot workspaces
2 parents 86dea1b + 0be6595 commit 7975b4c

File tree

7 files changed

+108
-27
lines changed

7 files changed

+108
-27
lines changed

workspaces/ballerina/ballerina-extension/src/features/ai/service/datamapper/datamapper.ts

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -346,10 +346,19 @@ export async function generateMappingCodeCore(mappingRequest: ProcessMappingPara
346346

347347
let customFunctionsTargetPath: string;
348348
let customFunctionsFileName: string;
349-
349+
350350
if (allMappingsRequest.customFunctionsFilePath) {
351-
customFunctionsTargetPath = determineCustomFunctionsPath(projectRoot, currentActiveFile);
352-
customFunctionsFileName = path.basename(customFunctionsTargetPath);
351+
const absoluteCustomFunctionsPath = determineCustomFunctionsPath(projectRoot, currentActiveFile);
352+
customFunctionsFileName = path.basename(absoluteCustomFunctionsPath);
353+
354+
// For workspace projects, make path relative to workspace root
355+
const workspacePath = context.workspacePath;
356+
if (workspacePath) {
357+
customFunctionsTargetPath = path.relative(workspacePath, absoluteCustomFunctionsPath);
358+
} else {
359+
// Normal project: use relative path from project root
360+
customFunctionsTargetPath = path.relative(projectRoot, absoluteCustomFunctionsPath);
361+
}
353362
}
354363

355364
// Check if mappings file and custom functions file are the same
@@ -395,7 +404,15 @@ export async function generateMappingCodeCore(mappingRequest: ProcessMappingPara
395404
);
396405
await new Promise((resolve) => setTimeout(resolve, 200));
397406

398-
let targetFilePath = path.join(projectRoot, mappingContext.filePath);
407+
// For workspace projects, compute relative file path from workspace root
408+
const workspacePath = context.workspacePath;
409+
let targetFilePath = mappingContext.filePath;
410+
411+
if (workspacePath) {
412+
// Workspace project: need to include package path prefix (e.g., "foo/mappings.bal")
413+
const absoluteFilePath = path.join(projectRoot, mappingContext.filePath);
414+
targetFilePath = path.relative(workspacePath, absoluteFilePath);
415+
}
399416

400417
const generatedSourceFiles = buildMappingFileArray(
401418
targetFilePath,
@@ -416,12 +433,12 @@ export async function generateMappingCodeCore(mappingRequest: ProcessMappingPara
416433

417434
if (isSameFile) {
418435
const mergedContent = `${generatedFunctionDefinition.source}\n${customContent}`;
419-
assistantResponse += `<code filename="${mappingContext.filePath}" type="ai_map">\n\`\`\`ballerina\n${mergedContent}\n\`\`\`\n</code>`;
436+
assistantResponse += `<code filename="${targetFilePath}" type="ai_map">\n\`\`\`ballerina\n${mergedContent}\n\`\`\`\n</code>`;
420437
} else {
421-
assistantResponse += `<code filename="${mappingContext.filePath}" type="ai_map">\n\`\`\`ballerina\n${generatedFunctionDefinition.source}\n\`\`\`\n</code>`;
438+
assistantResponse += `<code filename="${targetFilePath}" type="ai_map">\n\`\`\`ballerina\n${generatedFunctionDefinition.source}\n\`\`\`\n</code>`;
422439

423440
if (codeRepairResult.customFunctionsContent) {
424-
assistantResponse += `<code filename="${customFunctionsFileName}" type="ai_map">\n\`\`\`ballerina\n${codeRepairResult.customFunctionsContent}\n\`\`\`\n</code>`;
441+
assistantResponse += `<code filename="${customFunctionsTargetPath || customFunctionsFileName}" type="ai_map">\n\`\`\`ballerina\n${codeRepairResult.customFunctionsContent}\n\`\`\`\n</code>`;
425442
}
426443
}
427444

@@ -650,10 +667,19 @@ export async function generateInlineMappingCodeCore(inlineMappingRequest: Metada
650667

651668
let customFunctionsTargetPath: string | undefined;
652669
let customFunctionsFileName: string | undefined;
653-
670+
654671
if (inlineMappingsResult.allMappingsRequest.customFunctionsFilePath) {
655-
customFunctionsTargetPath = determineCustomFunctionsPath(projectRoot, targetFileName);
656-
customFunctionsFileName = path.basename(customFunctionsTargetPath);
672+
const absoluteCustomFunctionsPath = determineCustomFunctionsPath(projectRoot, targetFileName);
673+
customFunctionsFileName = path.basename(absoluteCustomFunctionsPath);
674+
675+
// For workspace projects, make path relative to workspace root
676+
const workspacePath = context.workspacePath;
677+
if (workspacePath) {
678+
customFunctionsTargetPath = path.relative(workspacePath, absoluteCustomFunctionsPath);
679+
} else {
680+
// Normal project: use relative path from project root
681+
customFunctionsTargetPath = path.relative(projectRoot, absoluteCustomFunctionsPath);
682+
}
657683
}
658684

659685
// Check if mappings file and custom functions file are the same
@@ -691,8 +717,17 @@ export async function generateInlineMappingCodeCore(inlineMappingRequest: Metada
691717
}, langClient, projectRoot);
692718
}
693719

720+
// For workspace projects, compute relative file path from workspace root
721+
let targetFilePath = path.relative(projectRoot, context.documentUri);
722+
const workspacePath = context.workspacePath;
723+
724+
if (workspacePath) {
725+
// Workspace project: make path relative to workspace root (e.g., "foo/mappings.bal")
726+
targetFilePath = path.relative(workspacePath, context.documentUri);
727+
}
728+
694729
const generatedSourceFiles = buildMappingFileArray(
695-
context.documentUri,
730+
targetFilePath,
696731
codeRepairResult.finalContent,
697732
customFunctionsTargetPath,
698733
codeRepairResult.customFunctionsContent,
@@ -718,12 +753,12 @@ export async function generateInlineMappingCodeCore(inlineMappingRequest: Metada
718753

719754
if (isSameFile) {
720755
const mergedCodeDisplay = customContent ? `${codeToDisplay}\n${customContent}` : codeToDisplay;
721-
assistantResponse += `<code filename="${targetFileName}" type="ai_map">\n\`\`\`ballerina\n${mergedCodeDisplay}\n\`\`\`\n</code>`;
756+
assistantResponse += `<code filename="${targetFilePath}" type="ai_map">\n\`\`\`ballerina\n${mergedCodeDisplay}\n\`\`\`\n</code>`;
722757
} else {
723-
assistantResponse += `<code filename="${targetFileName}" type="ai_map">\n\`\`\`ballerina\n${codeToDisplay}\n\`\`\`\n</code>`;
758+
assistantResponse += `<code filename="${targetFilePath}" type="ai_map">\n\`\`\`ballerina\n${codeToDisplay}\n\`\`\`\n</code>`;
724759

725760
if (codeRepairResult.customFunctionsContent) {
726-
assistantResponse += `<code filename="${customFunctionsFileName}" type="ai_map">\n\`\`\`ballerina\n${codeRepairResult.customFunctionsContent}\n\`\`\`\n</code>`;
761+
assistantResponse += `<code filename="${customFunctionsTargetPath || customFunctionsFileName}" type="ai_map">\n\`\`\`ballerina\n${codeRepairResult.customFunctionsContent}\n\`\`\`\n</code>`;
727762
}
728763
}
729764

workspaces/ballerina/ballerina-extension/src/features/ai/service/libs/text_editor_tool.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -305,10 +305,14 @@ export function createEditExecute(files: SourceFile[], updatedFileNames: string[
305305

306306
// Perform replacement
307307
let newContent: string;
308-
if (replace_all) {
309-
newContent = content.replaceAll(old_string, new_string);
308+
if (content.trim() === "" && old_string.trim() === "") {
309+
newContent = new_string;
310310
} else {
311-
newContent = content.replace(old_string, new_string);
311+
if (replace_all) {
312+
newContent = content.replaceAll(old_string, new_string);
313+
} else {
314+
newContent = content.replace(old_string, new_string);
315+
}
312316
}
313317

314318
updateOrCreateFile(files, file_path, newContent);
@@ -400,10 +404,14 @@ export function createMultiEditExecute(files: SourceFile[], updatedFileNames: st
400404
}
401405

402406
// Apply the edit to simulate the sequence
403-
if (edit.replace_all) {
404-
content = content.replaceAll(edit.old_string, edit.new_string);
407+
if (content.trim() === "" && edit.old_string.trim() === "") {
408+
content = edit.new_string;
405409
} else {
406-
content = content.replace(edit.old_string, edit.new_string);
410+
if (edit.replace_all) {
411+
content = content.replaceAll(edit.old_string, edit.new_string);
412+
} else {
413+
content = content.replace(edit.old_string, edit.new_string);
414+
}
407415
}
408416
}
409417

workspaces/ballerina/ballerina-extension/src/features/ai/service/test/function_tests.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,16 @@ import { getErrorMessage } from "../utils";
1919
import { generateTest, getDiagnostics } from "../../testGenerator";
2020
import { URI } from "vscode-uri";
2121
import * as fs from "fs";
22+
import * as path from "path";
2223
import { CopilotEventHandler, createWebviewEventHandler } from "../event";
2324
import { getCurrentProjectRoot } from "../../../../utils/project-utils";
25+
import { StateMachine } from "../../../../stateMachine";
2426

2527
// Core function test generation that emits events
2628
export async function generateFunctionTestsCore(
2729
params: TestGeneratorIntermediaryState,
2830
eventHandler: CopilotEventHandler
2931
): Promise<void> {
30-
const testPath = "tests/test.bal";
3132
const functionIdentifier = params.resourceFunction;
3233

3334
eventHandler({
@@ -48,6 +49,22 @@ export async function generateFunctionTestsCore(
4849
return;
4950
}
5051

52+
// Compute workspace-relative paths for test files (for display in UI)
53+
const context = StateMachine.context();
54+
const workspacePath = context.workspacePath;
55+
let testPathForDisplay = "tests/test.bal";
56+
let configPathForDisplay = "tests/Config.toml";
57+
58+
if (workspacePath) {
59+
// Workspace project: include package path prefix (e.g., "foo/tests/test.bal")
60+
const relativeProjectPath = path.relative(workspacePath, projectPath);
61+
testPathForDisplay = path.join(relativeProjectPath, "tests/test.bal");
62+
configPathForDisplay = path.join(relativeProjectPath, "tests/Config.toml");
63+
}
64+
65+
// Use project-relative path for file operations
66+
const testPath = "tests/test.bal";
67+
5168
const response = await generateTest(projectPath, {
5269
targetType: TestGenerationTarget.Function,
5370
targetIdentifier: functionIdentifier,
@@ -102,12 +119,12 @@ export async function generateFunctionTestsCore(
102119
});
103120
eventHandler({
104121
type: "content_block",
105-
content: `\n\n<code filename="${testPath}" type="test">\n\`\`\`ballerina\n${testCode}\n\`\`\`\n</code>`,
122+
content: `\n\n<code filename="${testPathForDisplay}" type="test">\n\`\`\`ballerina\n${testCode}\n\`\`\`\n</code>`,
106123
});
107124
if (testConfig) {
108125
eventHandler({
109126
type: "content_block",
110-
content: `\n\n<code filename="tests/Config.toml" type="test">\n\`\`\`ballerina\n${testConfig}\n\`\`\`\n</code>`,
127+
content: `\n\n<code filename="${configPathForDisplay}" type="test">\n\`\`\`ballerina\n${testConfig}\n\`\`\`\n</code>`,
111128
});
112129
}
113130

workspaces/ballerina/ballerina-extension/src/features/ai/service/test/test_plan.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import { generateTest, getDiagnostics } from "../../testGenerator";
2222
import { CopilotEventHandler, createWebviewEventHandler } from "../event";
2323
import { AIPanelAbortController } from "../../../../../src/rpc-managers/ai-panel/utils";
2424
import { getCurrentProjectRoot } from "../../../../utils/project-utils";
25+
import { StateMachine } from "../../../../stateMachine";
26+
import * as path from "path";
2527

2628
export interface TestPlanResponse {
2729
testPlan: string;
@@ -176,6 +178,20 @@ export async function generateTestPlanCore(
176178
eventHandler({ type: "error", content: getErrorMessage(error) });
177179
return;
178180
}
181+
182+
// Compute workspace-relative paths for test files
183+
const context = StateMachine.context();
184+
const workspacePath = context.workspacePath;
185+
let testPath = "tests/test.bal";
186+
let configPath = "tests/Config.toml";
187+
188+
if (workspacePath) {
189+
// Workspace project: include package path prefix (e.g., "foo/tests/test.bal")
190+
const relativeProjectPath = path.relative(workspacePath, projectPath);
191+
testPath = path.join(relativeProjectPath, "tests/test.bal");
192+
configPath = path.join(relativeProjectPath, "tests/Config.toml");
193+
}
194+
179195
const testResp = await generateTest(projectPath, {
180196
targetType: TestGenerationTarget.Service,
181197
targetIdentifier: target,
@@ -209,12 +225,12 @@ export async function generateTestPlanCore(
209225
});
210226
eventHandler({
211227
type: "content_block",
212-
content: `\n\n<code filename="tests/test.bal" type="test">\n\`\`\`ballerina\n${testCode}\n\`\`\`\n</code>`,
228+
content: `\n\n<code filename="${testPath}" type="test">\n\`\`\`ballerina\n${testCode}\n\`\`\`\n</code>`,
213229
});
214230
if (testConfig) {
215231
eventHandler({
216232
type: "content_block",
217-
content: `\n\n<code filename="tests/Config.toml" type="test">\n\`\`\`ballerina\n${testConfig}\n\`\`\`\n</code>`,
233+
content: `\n\n<code filename="${configPath}" type="test">\n\`\`\`ballerina\n${testConfig}\n\`\`\`\n</code>`,
218234
});
219235
}
220236
eventHandler({ type: "stop", command: Command.Tests });

workspaces/ballerina/ballerina-extension/src/rpc-managers/ai-panel/rpc-manager.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -662,7 +662,11 @@ export class AiPanelRpcManager implements AIPanelAPI {
662662

663663
async addFilesToProject(params: AddFilesToProjectRequest): Promise<boolean> {
664664
try {
665-
const projectPath = StateMachine.context().projectPath;
665+
let projectPath = StateMachine.context().projectPath;
666+
const workspacePath = StateMachine.context().workspacePath;
667+
if (workspacePath) {
668+
projectPath = workspacePath;
669+
}
666670

667671
const ballerinaProjectFile = path.join(projectPath, "Ballerina.toml");
668672
if (!fs.existsSync(ballerinaProjectFile)) {

workspaces/ballerina/ballerina-extension/src/stateMachine.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ const stateMachine = createMachine<MachineContext>(
253253
package: (context, event) => event.viewLocation?.package,
254254
view: (context, event) => event.viewLocation.view,
255255
documentUri: (context, event) => event.viewLocation.documentUri,
256+
projectPath: (context, event) => event.viewLocation?.projectPath || context?.projectPath,
256257
position: (context, event) => event.viewLocation.position,
257258
identifier: (context, event) => event.viewLocation.identifier,
258259
serviceType: (context, event) => event.viewLocation.serviceType,

workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChat/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -816,7 +816,7 @@ const AIChat: React.FC = () => {
816816
if (command === "ai_map") {
817817
const matchingFile = filePaths?.find(file => {
818818
const filePathName = file.filePath.split('/').pop();
819-
return filePathName === filePath;
819+
return filePathName === filePath.split('/').pop();
820820
});
821821
segmentText = matchingFile.content
822822
} else if (command === "test" || command === "type_creator") {

0 commit comments

Comments
 (0)