From 742168c944273d55ef9ec8bb4ed375ee9ba9b06b Mon Sep 17 00:00:00 2001 From: gigara Date: Mon, 10 Nov 2025 18:20:23 +0530 Subject: [PATCH 001/230] Fix openview failure issue --- .../mi/mi-core/src/state-machine-types.ts | 1 + workspaces/mi/mi-extension/src/RPCLayer.ts | 1 + .../mi/mi-extension/src/stateMachine.ts | 58 +++++++++---------- .../mi/mi-visualizer/src/Visualizer.tsx | 2 +- 4 files changed, 30 insertions(+), 32 deletions(-) diff --git a/workspaces/mi/mi-core/src/state-machine-types.ts b/workspaces/mi/mi-core/src/state-machine-types.ts index 4873e81c436..4328f4872b9 100644 --- a/workspaces/mi/mi-core/src/state-machine-types.ts +++ b/workspaces/mi/mi-core/src/state-machine-types.ts @@ -269,6 +269,7 @@ export interface VisualizerLocation { connectorData?: any[]; previousContext?: any; env?: { [key: string]: string | undefined }; + isLoading?: boolean; } export interface PopupVisualizerLocation extends VisualizerLocation { diff --git a/workspaces/mi/mi-extension/src/RPCLayer.ts b/workspaces/mi/mi-extension/src/RPCLayer.ts index 10727240ead..fe9c2c70fca 100644 --- a/workspaces/mi/mi-extension/src/RPCLayer.ts +++ b/workspaces/mi/mi-extension/src/RPCLayer.ts @@ -99,6 +99,7 @@ async function getContext(projectUri: string): Promise { diagnostics: context.diagnostics, dataMapperProps: context.dataMapperProps, errors: context.errors, + isLoading: context.isLoading, env: { MI_AUTH_ORG: process.env.MI_AUTH_ORG || '', MI_AUTH_CLIENT_ID: process.env.MI_AUTH_CLIENT_ID || '', diff --git a/workspaces/mi/mi-extension/src/stateMachine.ts b/workspaces/mi/mi-extension/src/stateMachine.ts index 765a408a093..00ddd3a66fa 100644 --- a/workspaces/mi/mi-extension/src/stateMachine.ts +++ b/workspaces/mi/mi-extension/src/stateMachine.ts @@ -55,7 +55,7 @@ const stateMachine = createMachine({ entry: () => log("State Machine: Entering 'initialize' state"), invoke: { id: 'checkProject', - src: (context) => checkIfMiProject(context.projectUri!), + src: (context) => checkIfMiProject(context.projectUri!, context.view ?? undefined), onDone: [ { target: 'environmentSetup', @@ -177,7 +177,8 @@ const stateMachine = createMachine({ target: 'ready.viewReady', cond: (context, event) => context.displayOverview === false, actions: assign({ - langClient: (context, event) => event.data + langClient: (context, event) => event.data, + isLoading: (context, event) => false }) } ], @@ -236,25 +237,17 @@ const stateMachine = createMachine({ invoke: { src: 'findView', onDone: { - target: 'viewStacking', + target: 'viewReady', actions: assign({ view: (context, event) => event.data.view, stNode: (context, event) => event.data.stNode, diagnostics: (context, event) => event.data.diagnostics, - dataMapperProps: (context, event) => event.data?.dataMapperProps + dataMapperProps: (context, event) => event.data?.dataMapperProps, + isLoading: (context, event) => false }) } } }, - viewStacking: { - entry: () => log("State Machine: Entering 'ready.viewStacking' state"), - invoke: { - src: 'updateStack', - onDone: { - target: "viewReady" - } - } - }, viewUpdated: { entry: () => log("State Machine: Entering 'ready.viewUpdated' state"), invoke: { @@ -264,7 +257,8 @@ const stateMachine = createMachine({ actions: assign({ stNode: (context, event) => event.data.stNode, diagnostics: (context, event) => event.data.diagnostics, - dataMapperProps: (context, event) => event.data?.dataMapperProps + dataMapperProps: (context, event) => event.data?.dataMapperProps, + isLoading: (context, event) => false }) } } @@ -286,7 +280,8 @@ const stateMachine = createMachine({ stNode: (context, event) => undefined, diagnostics: (context, event) => undefined, type: (context, event) => event.type, - previousContext: (context, event) => context + previousContext: (context, event) => context, + isLoading: (context, event) => true }) }, REPLACE_VIEW: { @@ -394,7 +389,7 @@ const stateMachine = createMachine({ waitForLS: (context, event) => { // replace this with actual promise that waits for LS to be ready return new Promise(async (resolve, reject) => { - log("Waiting for LS to be ready " + new Date().toLocaleTimeString()); + console.log("Waiting for LS to be ready " + new Date().toLocaleTimeString()); try { vscode.commands.executeCommand(COMMANDS.FOCUS_PROJECT_EXPLORER); const instance = await MILanguageClient.getInstance(context.projectUri!); @@ -406,9 +401,9 @@ const stateMachine = createMachine({ vscode.commands.executeCommand('setContext', 'MI.status', 'projectLoaded'); resolve(ls); - log("LS is ready " + new Date().toLocaleTimeString()); + console.log("LS is ready " + new Date().toLocaleTimeString()); } catch (error) { - log("Error occured while waiting for LS to be ready " + new Date().toLocaleTimeString()); + console.log("Error occured while waiting for LS to be ready " + new Date().toLocaleTimeString()); reject(error); } }); @@ -683,7 +678,7 @@ const stateMachine = createMachine({ // Create a service to interpret the machine const stateMachines: Map = new Map(); -export const getStateMachine = (projectUri: string): { +export const getStateMachine = (projectUri: string, context?: VisualizerLocation): { service: () => any; context: () => MachineContext; state: () => MachineStateValue; @@ -700,7 +695,8 @@ export const getStateMachine = (projectUri: string): { projectUri: projectUri, langClient: null, errors: [], - view: MACHINE_VIEW.Welcome + view: MACHINE_VIEW.Overview, + ...context })).start(); stateMachines.set(projectUri, stateService); } @@ -820,17 +816,17 @@ function updateProjectExplorer(location: VisualizerLocation | undefined) { vscode.commands.executeCommand(COMMANDS.REVEAL_TEST_PANE); } else if (projectRoot && !extension.preserveActivity) { location.projectUri = projectRoot; - if (!getStateMachine(projectRoot).context().isOldProject) { + if (!getStateMachine(projectRoot, location).context().isOldProject) { vscode.commands.executeCommand(COMMANDS.REVEAL_ITEM_COMMAND, location); } } } } -async function checkIfMiProject(projectUri: string) { - log(`Detecting project in ${projectUri} - ${new Date().toLocaleTimeString()}`); +async function checkIfMiProject(projectUri: string, view: MACHINE_VIEW = MACHINE_VIEW.Overview) { + console.log(`Detecting project in ${projectUri} - ${new Date().toLocaleTimeString()}`); - let isProject = false, isOldProject = false, isOldWorkspace = false, displayOverview = true, view = MACHINE_VIEW.Overview, isEnvironmentSetUp = false; + let isProject = false, isOldProject = false, isOldWorkspace = false, displayOverview = true, isEnvironmentSetUp = false; const customProps: any = {}; try { // Check for pom.xml files excluding node_modules directory @@ -839,7 +835,7 @@ async function checkIfMiProject(projectUri: string) { const pomContent = await fs.promises.readFile(pomFilePath, 'utf-8'); isProject = pomContent.includes('integration-project'); if (isProject) { - log("MI project detected in " + projectUri); + console.log("MI project detected in " + projectUri); } } @@ -901,11 +897,11 @@ async function checkIfMiProject(projectUri: string) { if (!isEnvironmentSetUp) { vscode.commands.executeCommand('setContext', 'MI.status', 'notSetUp'); } - // Log project path - log(`Current workspace path: ${projectUri}`); + // console.log project path + console.log(`Current workspace path: ${projectUri}`); } - log(`Project detection completed for path: ${projectUri} at ${new Date().toLocaleTimeString()}`); + console.log(`Project detection completed for path: ${projectUri} at ${new Date().toLocaleTimeString()}`); return { isProject, isOldProject, @@ -926,18 +922,18 @@ export async function isOldProjectOrWorkspace(projectUri: any) { if (projectFiles.length > 0) { if (await containsMultiModuleNatureInProjectFile(projectFiles[0].fsPath)) { isOldProject = true; - log("Integration Studio project detected"); + console.log("Integration Studio project detected"); } } else if (fs.existsSync(pomFilePath)) { if (await containsMultiModuleNatureInPomFile(pomFilePath)) { isOldProject = true; - log("Integration Studio project detected"); + console.log("Integration Studio project detected"); } } else if (fs.existsSync(projectUri)) { const foundOldProjects = await findMultiModuleProjectsInWorkspaceDir(projectUri); if (foundOldProjects.length > 0) { isOldWorkspace = true; - log("Integration Studio workspace detected"); + console.log("Integration Studio workspace detected"); } } return isOldProject || isOldWorkspace ? { isOldProject, isOldWorkspace } : false; diff --git a/workspaces/mi/mi-visualizer/src/Visualizer.tsx b/workspaces/mi/mi-visualizer/src/Visualizer.tsx index f3c824f46c8..648870207d2 100644 --- a/workspaces/mi/mi-visualizer/src/Visualizer.tsx +++ b/workspaces/mi/mi-visualizer/src/Visualizer.tsx @@ -93,7 +93,7 @@ export function Visualizer({ mode, swaggerData }: { mode: string, swaggerData?: rpcClient.getVisualizerState().then((initialState) => { setState(newState); process.env = initialState.env || {}; - if (Object.values(newState)?.[0] === 'viewReady') { + if (Object.values(newState)?.[0] === 'viewReady' && !initialState.isLoading) { setVisualizerState(initialState); } }); From 82124a6d9746106d9f6e0d70d593b70d4614c04b Mon Sep 17 00:00:00 2001 From: gigara Date: Mon, 10 Nov 2025 18:51:49 +0530 Subject: [PATCH 002/230] Add log level to settings --- workspaces/mi/mi-extension/package.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/workspaces/mi/mi-extension/package.json b/workspaces/mi/mi-extension/package.json index b75e950fbd1..7d813bd2078 100644 --- a/workspaces/mi/mi-extension/package.json +++ b/workspaces/mi/mi-extension/package.json @@ -162,6 +162,18 @@ "type": "boolean", "default": true, "description": "Update car plugin version automatically" + }, + "MI.logging.loggingLevel": { + "type": "string", + "enum": [ + "off", + "error", + "warn", + "debug" + ], + "default": "error", + "description": "The verbosity of logging. The Order is off < error < warn < debug.", + "scope": "window" } } }, From f8cbb09b0d359cd3d6d9e73c0a7b163e79d744d7 Mon Sep 17 00:00:00 2001 From: gigara Date: Mon, 10 Nov 2025 18:51:59 +0530 Subject: [PATCH 003/230] Print logs based on the log level --- .../mi-extension/src/debugger/debugHelper.ts | 32 +++++++------- .../mi/mi-extension/src/debugger/debugger.ts | 36 +++++++-------- .../mi/mi-extension/src/debugger/tasks.ts | 6 +-- .../mi/mi-extension/src/stateMachine.ts | 44 +++++++++---------- workspaces/mi/mi-extension/src/util/logger.ts | 23 ++++++++-- 5 files changed, 78 insertions(+), 63 deletions(-) diff --git a/workspaces/mi/mi-extension/src/debugger/debugHelper.ts b/workspaces/mi/mi-extension/src/debugger/debugHelper.ts index 361f686fe4f..8787bb9ffea 100644 --- a/workspaces/mi/mi-extension/src/debugger/debugHelper.ts +++ b/workspaces/mi/mi-extension/src/debugger/debugHelper.ts @@ -29,7 +29,7 @@ import axios from 'axios'; import * as net from 'net'; import { MACHINE_VIEW } from '@wso2/mi-core'; import { getStateMachine } from '../stateMachine'; -import { ERROR_LOG, INFO_LOG, logDebug } from '../util/logger'; +import { logDebug, LogLevel } from '../util/logger'; import * as toml from "@iarna/toml"; import { DebuggerConfig } from './config'; import { ChildProcess } from 'child_process'; @@ -104,7 +104,7 @@ export function checkServerReadiness(): Promise { .then((response: { status: number; data: any; }) => { if (response.status === 200) { if (response.data.status === 'ready') { - logDebug('Server is ready with CApp deployed', INFO_LOG); + logDebug('Server is ready with CApp deployed', LogLevel.INFO); resolve(); } else { reject(response.data.status); @@ -115,7 +115,7 @@ export function checkServerReadiness(): Promise { if (elapsedTime < maxTimeout) { setTimeout(checkReadiness, retryInterval); } else { - logDebug('Timeout reached while checking server readiness', ERROR_LOG); + logDebug('Timeout reached while checking server readiness', LogLevel.ERROR); reject('CApp has encountered deployment issues. Please refer to the output for error logs.'); } } @@ -126,7 +126,7 @@ export function checkServerReadiness(): Promise { setTimeout(checkReadiness, retryInterval); } else { const errorMsg = error?.errors[0]?.message; - logDebug(`Error while checking for Server readiness: ${errorMsg}`, ERROR_LOG); + logDebug(`Error while checking for Server readiness: ${errorMsg}`, LogLevel.ERROR); reject(`CApp has encountered deployment issues. Please refer to the output for error logs.`); } }); @@ -224,7 +224,7 @@ export async function executeBuildTask(projectUri: string, serverPath: string, s const sourceFiles = await getCarFiles(targetDirectory); if (sourceFiles.length === 0) { const errorMessage = "No .car files were found in the target directory. Built without copying to the server's carbonapps directory."; - logDebug(errorMessage, ERROR_LOG); + logDebug(errorMessage, LogLevel.ERROR); reject(errorMessage); } else { const targetPath = path.join(serverPath, 'repository', 'deployment', 'server', 'carbonapps'); @@ -233,7 +233,7 @@ export async function executeBuildTask(projectUri: string, serverPath: string, s fs.copyFileSync(sourceFile.fsPath, destinationFile); DebuggerConfig.setCopiedCapp(destinationFile); }); - logDebug('Build and copy tasks executed successfully', INFO_LOG); + logDebug('Build and copy tasks executed successfully', LogLevel.INFO); resolve(); } } catch (err) { @@ -426,7 +426,7 @@ export async function executeTasks(projectUri: string, serverPath: string, isDeb resolve(); // Proceed with connecting to the port } else { - logDebug('Server is running, but the debugger command port not acitve', ERROR_LOG); + logDebug('Server is running, but the debugger command port not acitve', LogLevel.ERROR); reject(`Server command port isn't actively listening. Stop any running MI servers and restart the debugger.`); } }); @@ -444,7 +444,7 @@ export async function executeTasks(projectUri: string, serverPath: string, isDeb } }).catch((error) => { reject(error); - logDebug(`Error executing BuildTask: ${error}`, ERROR_LOG); + logDebug(`Error executing BuildTask: ${error}`, LogLevel.ERROR); }); }); @@ -457,7 +457,7 @@ export async function executeTasks(projectUri: string, serverPath: string, isDeb resolve(); // Proceed with connecting to the port } else { - logDebug(`The ${DebuggerConfig.getCommandPort()} port is not actively listening or the timeout has been reached.`, ERROR_LOG); + logDebug(`The ${DebuggerConfig.getCommandPort()} port is not actively listening or the timeout has been reached.`, LogLevel.ERROR); reject(`Server command port isn't actively listening. Stop any running MI servers and restart the debugger.`); } }); @@ -519,7 +519,7 @@ export async function deleteCapp(serverPath: string): Promise { resolve(); } } catch (err) { - logDebug(`Error deleting Capp: ${err}`, ERROR_LOG); + logDebug(`Error deleting Capp: ${err}`, LogLevel.ERROR); reject(err); } }); @@ -534,7 +534,7 @@ export async function deleteCopiedCapAndLibs() { await deleteSpecificFiles(copiedLibs); } catch (err) { - logDebug(`Failed to delete Capp and Libs: ${err}`, ERROR_LOG); + logDebug(`Failed to delete Capp and Libs: ${err}`, LogLevel.ERROR); throw err; } } @@ -549,7 +549,7 @@ export async function deleteSpecificFiles(filesToDelete: string[]): Promise { if (err) { - logDebug(`Error reading the micro-integrator-debug.bat file: ${err}`, ERROR_LOG); + logDebug(`Error reading the micro-integrator-debug.bat file: ${err}`, LogLevel.ERROR); reject(`Error while reading the micro-integrator-debug.bat file: ${err}`); return; } @@ -590,7 +590,7 @@ export function createTempDebugBatchFile(batchFilePath: string, binPath: string) fs.writeFile(destFilePath, updatedContent, 'utf8', (err) => { if (err) { - logDebug(`Error writing the micro-integrator-debug.bat file: ${err}`, ERROR_LOG); + logDebug(`Error writing the micro-integrator-debug.bat file: ${err}`, LogLevel.ERROR); reject(`Error while updating the micro-integrator-debug.bat file: ${err}`); return; } @@ -622,7 +622,7 @@ export async function readPortOffset(serverConfigPath: string): Promise { const workspace = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(filePath)); if (!workspace) { - logDebug(`No workspace found for path: ${filePath}`, ERROR_LOG); + logDebug(`No workspace found for path: ${filePath}`, LogLevel.ERROR); throw new Error(`No workspace found for path: ${filePath}`); } const projectUri = workspace.uri.fsPath; @@ -391,17 +391,17 @@ export class Debugger extends EventEmitter { } resolve(); }).catch((error) => { - logDebug(`Error while sending the resume command: ${error}`, ERROR_LOG); + logDebug(`Error while sending the resume command: ${error}`, LogLevel.ERROR); reject(`Error while resuming the debugger server: ${error}`); }); }).catch((error) => { - logDebug(`Error while checking server readiness: ${error}`, ERROR_LOG); + logDebug(`Error while checking server readiness: ${error}`, LogLevel.ERROR); reject(error); }); }).catch((error) => { - logDebug(`Error while connecting the debugger to the MI server: ${error}`, ERROR_LOG); + logDebug(`Error while connecting the debugger to the MI server: ${error}`, LogLevel.ERROR); reject(`Error while connecting the debugger to the MI server: ${error}`); }); }); @@ -419,7 +419,7 @@ export class Debugger extends EventEmitter { // Error handling for the command client this.commandClient?.on('error', (error) => { - logDebug(`Command client error: ${error}`, ERROR_LOG); + logDebug(`Command client error: ${error}`, LogLevel.ERROR); reject(error); // Reject the promise if there's an error }); @@ -430,7 +430,7 @@ export class Debugger extends EventEmitter { // Error handling for the event client this.eventClient?.on('error', (error) => { - logDebug(`Event client error: ${error}`, ERROR_LOG); + logDebug(`Event client error: ${error}`, LogLevel.ERROR); reject(error); }); @@ -450,7 +450,7 @@ export class Debugger extends EventEmitter { const message = incompleteMessage.slice(0, newlineIndex); // Call a function with the received message - logDebug(`Event received: ${message}`, INFO_LOG); + logDebug(`Event received: ${message}`, LogLevel.INFO); // convert to eventData to json const eventDataJson = JSON.parse(message); @@ -591,7 +591,7 @@ export class Debugger extends EventEmitter { let incompleteMessage = ''; // Send request on the command port - logDebug(`Command: ${request}`, INFO_LOG); + logDebug(`Command: ${request}`, LogLevel.INFO); this.commandClient?.write(request); // Listen for response from the command port @@ -609,7 +609,7 @@ export class Debugger extends EventEmitter { const message = incompleteMessage.slice(0, newlineIndex); // Call a function with the received message - logDebug(`Command response: ${message}`, INFO_LOG); + logDebug(`Command response: ${message}`, LogLevel.INFO); resolve(message); // Resolve the promise with the message // Remove the processed message from incompleteMessage @@ -644,7 +644,7 @@ export class Debugger extends EventEmitter { variables.push(jsonResponse); } catch (error) { - logDebug(`Error sending properties-command for ${context}: ${error}`, ERROR_LOG); + logDebug(`Error sending properties-command for ${context}: ${error}`, LogLevel.ERROR); } } return variables; diff --git a/workspaces/mi/mi-extension/src/debugger/tasks.ts b/workspaces/mi/mi-extension/src/debugger/tasks.ts index a412060b0b5..3ea0770875a 100644 --- a/workspaces/mi/mi-extension/src/debugger/tasks.ts +++ b/workspaces/mi/mi-extension/src/debugger/tasks.ts @@ -20,7 +20,7 @@ import * as path from 'path'; import * as vscode from 'vscode'; import * as fs from 'fs'; import { createTempDebugBatchFile, setJavaHomeInEnvironmentAndPath } from './debugHelper'; -import { ERROR_LOG, logDebug } from '../util/logger'; +import { LogLevel, logDebug } from '../util/logger'; import { Uri, workspace } from "vscode"; import { MVN_COMMANDS } from "../constants"; @@ -151,7 +151,7 @@ export function getStopTask(serverPath: string): vscode.Task | undefined { const command = `${binPath} stop`; if (!fs.existsSync(binPath)) { - logDebug(`${binPath} does not exist`, ERROR_LOG); + logDebug(`${binPath} does not exist`, LogLevel.ERROR); return; } @@ -177,7 +177,7 @@ export function getStopCommand(serverPath: string): string | undefined { const command = `"${binPath}" stop`; if (!fs.existsSync(binPath)) { - logDebug(`${binPath} does not exist`, ERROR_LOG); + logDebug(`${binPath} does not exist`, LogLevel.ERROR); return; } diff --git a/workspaces/mi/mi-extension/src/stateMachine.ts b/workspaces/mi/mi-extension/src/stateMachine.ts index 00ddd3a66fa..94e29008ffd 100644 --- a/workspaces/mi/mi-extension/src/stateMachine.ts +++ b/workspaces/mi/mi-extension/src/stateMachine.ts @@ -21,7 +21,7 @@ import { history } from './history/activator'; import { COMMANDS } from './constants'; import { activateProjectExplorer } from './project-explorer/activate'; import { MockService, STNode, UnitTest, Task, InboundEndpoint } from '../../syntax-tree/lib/src'; -import { log } from './util/logger'; +import { logDebug } from './util/logger'; import { deriveConfigName, getSources } from './util/dataMapper'; import { fileURLToPath } from 'url'; import path = require('path'); @@ -52,7 +52,7 @@ const stateMachine = createMachine({ }, states: { initialize: { - entry: () => log("State Machine: Entering 'initialize' state"), + entry: () => logDebug("State Machine: Entering 'initialize' state"), invoke: { id: 'checkProject', src: (context) => checkIfMiProject(context.projectUri!, context.view ?? undefined), @@ -119,7 +119,7 @@ const stateMachine = createMachine({ } }, projectDetected: { - entry: () => log("State Machine: Entering 'projectDetected' state"), + entry: () => logDebug("State Machine: Entering 'projectDetected' state"), invoke: { src: 'openWebPanel', onDone: { @@ -128,7 +128,7 @@ const stateMachine = createMachine({ } }, oldProjectDetected: { - entry: () => log("State Machine: Entering 'oldProjectDetected' state"), + entry: () => logDebug("State Machine: Entering 'oldProjectDetected' state"), invoke: { src: 'openWebPanel', onDone: { @@ -137,11 +137,11 @@ const stateMachine = createMachine({ } }, oldWorkspaceDetected: { - entry: () => log("State Machine: Entering 'oldWorkspaceDetected' state"), + entry: () => logDebug("State Machine: Entering 'oldWorkspaceDetected' state"), initial: "viewLoading", states: { viewLoading: { - entry: () => log("State Machine: Entering 'oldWorkspaceDetected.viewLoading' state"), + entry: () => logDebug("State Machine: Entering 'oldWorkspaceDetected.viewLoading' state"), invoke: [ { src: 'openWebPanel', @@ -152,7 +152,7 @@ const stateMachine = createMachine({ ] }, viewReady: { - entry: () => log("State Machine: Entering 'oldWorkspaceDetected.viewReady' state"), + entry: () => logDebug("State Machine: Entering 'oldWorkspaceDetected.viewReady' state"), on: { REFRESH_ENVIRONMENT: { target: '#mi.initialize' @@ -162,7 +162,7 @@ const stateMachine = createMachine({ } }, lsInit: { - entry: () => log("State Machine: Entering 'lsInit' state"), + entry: () => logDebug("State Machine: Entering 'lsInit' state"), invoke: { src: 'waitForLS', onDone: [ @@ -192,11 +192,11 @@ const stateMachine = createMachine({ } }, ready: { - entry: () => log("State Machine: Entering 'ready' state"), + entry: () => logDebug("State Machine: Entering 'ready' state"), initial: 'activateOtherFeatures', states: { activateOtherFeatures: { - entry: () => log("State Machine: Entering 'ready.activateOtherFeatures' state"), + entry: () => logDebug("State Machine: Entering 'ready.activateOtherFeatures' state"), invoke: { src: 'activateOtherFeatures', onDone: { @@ -205,7 +205,7 @@ const stateMachine = createMachine({ } }, viewLoading: { - entry: () => log("State Machine: Entering 'ready.viewLoading' state"), + entry: () => logDebug("State Machine: Entering 'ready.viewLoading' state"), invoke: { src: 'openWebPanel', onDone: [ @@ -221,7 +221,7 @@ const stateMachine = createMachine({ } }, resolveMissingDependencies: { - entry: () => log("State Machine: Entering 'ready.resolveMissingDependencies' state"), + entry: () => logDebug("State Machine: Entering 'ready.resolveMissingDependencies' state"), invoke: { src: 'resolveMissingDependencies', onDone: { @@ -233,7 +233,7 @@ const stateMachine = createMachine({ } }, viewFinding: { - entry: () => log("State Machine: Entering 'ready.viewFinding' state"), + entry: () => logDebug("State Machine: Entering 'ready.viewFinding' state"), invoke: { src: 'findView', onDone: { @@ -249,7 +249,7 @@ const stateMachine = createMachine({ } }, viewUpdated: { - entry: () => log("State Machine: Entering 'ready.viewUpdated' state"), + entry: () => logDebug("State Machine: Entering 'ready.viewUpdated' state"), invoke: { src: 'findView', onDone: { @@ -264,7 +264,7 @@ const stateMachine = createMachine({ } }, viewReady: { - entry: () => log("State Machine: Entering 'ready.viewReady' state"), + entry: () => logDebug("State Machine: Entering 'ready.viewReady' state"), on: { OPEN_VIEW: { target: "viewLoading", @@ -317,17 +317,17 @@ const stateMachine = createMachine({ } }, disabled: { - entry: () => log("State Machine: Entering 'disabled' state"), + entry: () => logDebug("State Machine: Entering 'disabled' state"), invoke: { src: 'disableExtension', }, }, newProject: { - entry: () => log("State Machine: Entering 'newProject' state"), + entry: () => logDebug("State Machine: Entering 'newProject' state"), initial: "viewLoading", states: { viewLoading: { - entry: () => log("State Machine: Entering 'newProject.viewLoading' state"), + entry: () => logDebug("State Machine: Entering 'newProject.viewLoading' state"), invoke: { src: 'openWebPanel', data: (context, event) => ({ context, event, setTitle: true }), @@ -337,7 +337,7 @@ const stateMachine = createMachine({ } }, viewReady: { - entry: () => log("State Machine: Entering 'newProject.viewReady' state"), + entry: () => logDebug("State Machine: Entering 'newProject.viewReady' state"), on: { OPEN_VIEW: { target: "viewLoading", @@ -350,11 +350,11 @@ const stateMachine = createMachine({ } }, environmentSetup: { - entry: () => log("State Machine: Entering 'environmentSetup' state"), + entry: () => logDebug("State Machine: Entering 'environmentSetup' state"), initial: "viewLoading", states: { viewLoading: { - entry: () => log("State Machine: Entering 'environmentSetup.viewLoading' state"), + entry: () => logDebug("State Machine: Entering 'environmentSetup.viewLoading' state"), invoke: [ { src: 'openWebPanel', @@ -371,7 +371,7 @@ const stateMachine = createMachine({ ] }, viewReady: { - entry: () => log("State Machine: Entering 'environmentSetup.viewReady' state"), + entry: () => logDebug("State Machine: Entering 'environmentSetup.viewReady' state"), on: { REFRESH_ENVIRONMENT: { target: '#mi.initialize' diff --git a/workspaces/mi/mi-extension/src/util/logger.ts b/workspaces/mi/mi-extension/src/util/logger.ts index ffa428f97ad..53ffc477c04 100644 --- a/workspaces/mi/mi-extension/src/util/logger.ts +++ b/workspaces/mi/mi-extension/src/util/logger.ts @@ -19,8 +19,13 @@ import * as vscode from 'vscode'; export const outputChannel = vscode.window.createOutputChannel("WSO2 Integrator: MI"); -export const ERROR_LOG = 'ERROR'; -export const INFO_LOG = 'INFO'; + +export enum LogLevel { + ERROR = 'error', + WARN = 'warn', + INFO = 'info', + DEBUG = 'debug' +} function withNewLine(value: string) { if (typeof value === 'string' && !value.endsWith('\n')) { @@ -41,8 +46,18 @@ export function logWithDebugLevel(message: string, debugLabel: string, logLevel: outputChannel.append(output); } -export function logDebug(message: string, logLevel: string): void { - logWithDebugLevel(message, 'MI Debug', logLevel); +export function logDebug(message: string, logLevel: LogLevel = LogLevel.DEBUG): void { + const config = vscode.workspace.getConfiguration('MI'); + const configuredLevel = config.get('logging.loggingLevel'); + + if (configuredLevel === 'OFF') { + return; + } + // Only log if the message's level is >= configured level + const levels = [LogLevel.ERROR, LogLevel.WARN, LogLevel.INFO, LogLevel.DEBUG]; + if (levels.indexOf(logLevel) <= levels.indexOf(configuredLevel as LogLevel)) { + logWithDebugLevel(message, 'MI Debug', logLevel); + } } export function getOutputChannel() { From 03e6290b38f287daf0504c70f68e2436b689552c Mon Sep 17 00:00:00 2001 From: gigara Date: Mon, 10 Nov 2025 21:25:15 +0530 Subject: [PATCH 004/230] fix typo --- workspaces/mi/mi-extension/src/util/logger.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workspaces/mi/mi-extension/src/util/logger.ts b/workspaces/mi/mi-extension/src/util/logger.ts index 53ffc477c04..387f88dc5ef 100644 --- a/workspaces/mi/mi-extension/src/util/logger.ts +++ b/workspaces/mi/mi-extension/src/util/logger.ts @@ -50,7 +50,7 @@ export function logDebug(message: string, logLevel: LogLevel = LogLevel.DEBUG): const config = vscode.workspace.getConfiguration('MI'); const configuredLevel = config.get('logging.loggingLevel'); - if (configuredLevel === 'OFF') { + if (configuredLevel === 'off') { return; } // Only log if the message's level is >= configured level From 6c4bbbadb225449c5e27f9ecec63c965c4a23c08 Mon Sep 17 00:00:00 2001 From: gigara Date: Mon, 10 Nov 2025 21:26:53 +0530 Subject: [PATCH 005/230] Add 'info' log level to logging configuration description --- workspaces/mi/mi-extension/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/workspaces/mi/mi-extension/package.json b/workspaces/mi/mi-extension/package.json index 7d813bd2078..e436ee7469a 100644 --- a/workspaces/mi/mi-extension/package.json +++ b/workspaces/mi/mi-extension/package.json @@ -169,10 +169,11 @@ "off", "error", "warn", + "info", "debug" ], "default": "error", - "description": "The verbosity of logging. The Order is off < error < warn < debug.", + "description": "The verbosity of logging. The Order is off < error < warn < info < debug.", "scope": "window" } } From 5797c8b91a85f997ea175edaeffc575f3339e48b Mon Sep 17 00:00:00 2001 From: gigara Date: Mon, 10 Nov 2025 21:53:37 +0530 Subject: [PATCH 006/230] Fix popup state machine --- .../mi/mi-extension/src/stateMachinePopup.ts | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/workspaces/mi/mi-extension/src/stateMachinePopup.ts b/workspaces/mi/mi-extension/src/stateMachinePopup.ts index 3d0311adc10..40df56a396c 100644 --- a/workspaces/mi/mi-extension/src/stateMachinePopup.ts +++ b/workspaces/mi/mi-extension/src/stateMachinePopup.ts @@ -23,6 +23,7 @@ import { POPUP_EVENT_TYPE, PopupVisualizerLocation, PopupMachineStateValue, onPa import { VisualizerWebview } from './visualizer/webview'; import { RPCLayer } from './RPCLayer'; import { getStateMachine } from './stateMachine'; +import { logDebug } from './util/logger'; interface PopupMachineContext extends PopupVisualizerLocation { errorCode: string | null; @@ -41,6 +42,7 @@ const stateMachinePopup = createMachine({ }, states: { initialize: { + entry: () => logDebug('Popup State Machine: Entering initialize state'), invoke: { src: 'initializeData', onDone: { @@ -58,6 +60,7 @@ const stateMachinePopup = createMachine({ } }, ready: { + entry: () => logDebug('Popup State Machine: Entering ready state'), on: { OPEN_VIEW: { target: "open", @@ -74,6 +77,7 @@ const stateMachinePopup = createMachine({ initial: "active", states: { active: { + entry: () => logDebug('Popup State Machine: Entering active state'), on: { OPEN_VIEW: { target: "reopen", @@ -94,6 +98,7 @@ const stateMachinePopup = createMachine({ } }, reopen: { + entry: () => logDebug('Popup State Machine: Entering reopen state'), invoke: { src: 'initializeData', onDone: { @@ -105,6 +110,7 @@ const stateMachinePopup = createMachine({ } }, notify: { + entry: () => logDebug('Popup State Machine: Entering notify state'), invoke: { src: 'notifyChange', onDone: { @@ -115,6 +121,7 @@ const stateMachinePopup = createMachine({ }, }, disabled: { + entry: () => logDebug('Popup State Machine: Entering disabled state'), invoke: { src: 'disableExtension' }, @@ -214,5 +221,17 @@ export const deletePopupStateMachine = (projectUri: string) => { }; export function openPopupView(projectUri: string, type: POPUP_EVENT_TYPE, viewLocation?: PopupVisualizerLocation) { - getPopupStateMachine(projectUri).service().send({ type: type, viewLocation: viewLocation }); + const stateMachine = getPopupStateMachine(projectUri); + const state = stateMachine.state(); + if (state === 'initialize') { + const listener = (state: { value: string; }) => { + if (state?.value === "ready") { + stateMachine.service().send({ type: type, viewLocation: viewLocation }); + stateMachine.service().off(listener); + } + }; + stateMachine.service().onTransition(listener); + } else { + stateMachine.service().send({ type: type, viewLocation: viewLocation }); + } } From db606bb418db937d7c57c72f0248862238159cdf Mon Sep 17 00:00:00 2001 From: gigara Date: Mon, 10 Nov 2025 22:14:00 +0530 Subject: [PATCH 007/230] Add new test case --- .../artifactTests/artifact.spec.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/workspaces/mi/mi-extension/src/test/e2e-playwright-tests/artifactTests/artifact.spec.ts b/workspaces/mi/mi-extension/src/test/e2e-playwright-tests/artifactTests/artifact.spec.ts index 460ff17400e..79f66c110d2 100644 --- a/workspaces/mi/mi-extension/src/test/e2e-playwright-tests/artifactTests/artifact.spec.ts +++ b/workspaces/mi/mi-extension/src/test/e2e-playwright-tests/artifactTests/artifact.spec.ts @@ -36,7 +36,7 @@ import { EventIntegration } from '../components/ArtifactTest/EventIntegration'; import { ImportArtifact } from '../components/ImportArtifact'; import path from 'path'; import { ProjectExplorer } from '../components/ProjectExplorer'; -const filePath = path.join( __dirname, '..', 'components', 'ArtifactTest', 'data', 'importApi_v1.0.0.xml'); +const filePath = path.join(__dirname, '..', 'components', 'ArtifactTest', 'data', 'importApi_v1.0.0.xml'); export default function createTests() { test.describe('Artifact Tests', { @@ -128,7 +128,14 @@ export default function createTests() { await test.step('Open Diagram View for API', async () => { console.log('Opening Diagram View for API'); await api.openDiagramView("NewOpenAPI" + testAttempt + ":v1.0.27-SNAPSHOT", "/pet/findByStatus"); - // Collapese APIs section + + await page.page.waitForTimeout(2000); + await page.executePaletteCommand('View: Close All Editors'); + console.log("Closed editor groups"); + console.log('Opening Diagram View for API again without existing webview'); + await api.openDiagramView("NewOpenAPI" + testAttempt + ":v1.0.27-SNAPSHOT", "/pet/findByStatus"); + + // Collapse APIs section const projectExplorer = new ProjectExplorer(page.page); await projectExplorer.findItem(['Project testProject', 'APIs', "NewOpenAPI" + testAttempt + ":v1.0.27-SNAPSHOT"], true); await projectExplorer.findItem(['Project testProject', 'APIs'], true); @@ -364,9 +371,9 @@ export default function createTests() { await ms.createMessageStoreFromProjectExplorer(msName); // Close Message Stores const projectExplorer = new ProjectExplorer(page.page); - await projectExplorer.findItem(['Project testProject', 'Other Artifacts', 'Message Stores'], true); + await projectExplorer.findItem(['Project testProject', 'Other Artifacts', 'Message Stores'], true); }); - + }); test('Message Processor Tests', async () => { @@ -612,7 +619,7 @@ export default function createTests() { await projectExplorer.findItem(['Project testProject', 'Other Artifacts', 'Proxy Services'], true); }); - test ('Import Artifact', async () => { + test('Import Artifact', async () => { await test.step('Import API Artifact', async () => { const importArtifact = new ImportArtifact(page.page); await importArtifact.init(); From aad519481af14d41cc53887e4459ef4b5cd8c52b Mon Sep 17 00:00:00 2001 From: gigara Date: Mon, 10 Nov 2025 23:21:08 +0530 Subject: [PATCH 008/230] Add default setting for secondary sidebar visibility in VSBrowser --- workspaces/common-libs/playwright-vscode-tester/src/browser.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/workspaces/common-libs/playwright-vscode-tester/src/browser.ts b/workspaces/common-libs/playwright-vscode-tester/src/browser.ts index 969b6b50717..4df5089d450 100644 --- a/workspaces/common-libs/playwright-vscode-tester/src/browser.ts +++ b/workspaces/common-libs/playwright-vscode-tester/src/browser.ts @@ -94,7 +94,8 @@ export class VSBrowser { "extensions.autoUpdate": false, "chat.disableAIFeatures": true, "github.copilot.enable": false, - "github.copilot.chat.enable": false + "github.copilot.chat.enable": false, + "workbench.secondarySideBar.defaultVisibility": "hidden" }; if (Object.keys(this.customSettings).length > 0) { console.log('Detected user defined code settings'); From f813fcff2d1729250f36187d290bd273f12b5232 Mon Sep 17 00:00:00 2001 From: gigara Date: Tue, 11 Nov 2025 13:21:22 +0530 Subject: [PATCH 009/230] Fix test failure --- .../e2e-playwright-tests/components/ArtifactTest/APITests.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/workspaces/mi/mi-extension/src/test/e2e-playwright-tests/components/ArtifactTest/APITests.ts b/workspaces/mi/mi-extension/src/test/e2e-playwright-tests/components/ArtifactTest/APITests.ts index 84b2f61efbc..50c31504b8e 100644 --- a/workspaces/mi/mi-extension/src/test/e2e-playwright-tests/components/ArtifactTest/APITests.ts +++ b/workspaces/mi/mi-extension/src/test/e2e-playwright-tests/components/ArtifactTest/APITests.ts @@ -401,12 +401,11 @@ export class API { public async openDiagramView(name: string, resourcePath : string) { const projectExplorer = new ProjectExplorer(this._page); - await projectExplorer.goToOverview("testProject"); await projectExplorer.findItem(['Project testProject', 'APIs', name, resourcePath], true); const webView = await switchToIFrame('Resource View', this._page); if (!webView) { throw new Error("Failed to switch to Resource View iframe"); } - await webView.getByText('Start').click(); + await webView.getByText('Start').waitFor(); } } From 68bfd5fc6f9cdc3a08037e20283b592ed1b65eb0 Mon Sep 17 00:00:00 2001 From: gigara Date: Tue, 11 Nov 2025 13:21:32 +0530 Subject: [PATCH 010/230] Update tsconfig.json to exclude additional test resources --- workspaces/mi/mi-extension/tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/workspaces/mi/mi-extension/tsconfig.json b/workspaces/mi/mi-extension/tsconfig.json index af0b1cb1cb8..fb41a722496 100644 --- a/workspaces/mi/mi-extension/tsconfig.json +++ b/workspaces/mi/mi-extension/tsconfig.json @@ -20,6 +20,7 @@ }, "exclude": [ "./resources", - "./src/test/e2e-playwright-tests/data" + "./src/test/e2e-playwright-tests/data", + "./src/test/test-resources" ] } \ No newline at end of file From 1df10950a9ce4dbc88f4f72e0793d5e78cf5b509 Mon Sep 17 00:00:00 2001 From: Kanushka Gayan Date: Wed, 12 Nov 2025 00:46:24 +0530 Subject: [PATCH 011/230] Add dependency pull progress notification and enhance build command execution - Introduced notification type in . - Updated to utilize for executing build commands with output streaming and progress notifications. - Created in for logging build process output. - Implemented progress reporting in the visualizer for dependency pulling. - Adjusted UI components to reflect changes in loading states and improved user feedback during dependency resolution. --- .../ballerina-core/src/state-machine-types.ts | 1 + .../ballerina-extension/src/stateMachine.ts | 72 +++++++-------- .../ballerina-extension/src/utils/logger.ts | 1 + .../src/utils/runCommand.ts | 87 +++++++++++++++++++ .../src/views/visualizer/webview.ts | 14 ++- .../src/BallerinaRpcClient.ts | 7 +- .../ballerina-visualizer/src/Visualizer.tsx | 36 ++++---- .../src/components/DownloadIcon/index.tsx | 7 +- .../Connection/AddConnectionWizard/index.tsx | 8 +- 9 files changed, 161 insertions(+), 72 deletions(-) diff --git a/workspaces/ballerina/ballerina-core/src/state-machine-types.ts b/workspaces/ballerina/ballerina-core/src/state-machine-types.ts index 2b44f15d986..15cf641e327 100644 --- a/workspaces/ballerina/ballerina-core/src/state-machine-types.ts +++ b/workspaces/ballerina/ballerina-core/src/state-machine-types.ts @@ -282,6 +282,7 @@ export const onMigrationToolStateChanged: NotificationType = { method: ' export const projectContentUpdated: NotificationType = { method: 'projectContentUpdated' }; export const getVisualizerLocation: RequestType = { method: 'getVisualizerLocation' }; export const webviewReady: NotificationType = { method: `webviewReady` }; +export const dependencyPullProgress: NotificationType = { method: 'dependencyPullProgress' }; // Artifact updated request and notification export const onArtifactUpdatedNotification: NotificationType = { method: 'onArtifactUpdatedNotification' }; diff --git a/workspaces/ballerina/ballerina-extension/src/stateMachine.ts b/workspaces/ballerina/ballerina-extension/src/stateMachine.ts index 340671dfac4..98c607b5057 100644 --- a/workspaces/ballerina/ballerina-extension/src/stateMachine.ts +++ b/workspaces/ballerina/ballerina-extension/src/stateMachine.ts @@ -2,10 +2,10 @@ import { ExtendedLangClient } from './core'; import { createMachine, assign, interpret } from 'xstate'; import { activateBallerina } from './extension'; -import { EVENT_TYPE, SyntaxTree, History, MachineStateValue, IUndoRedoManager, VisualizerLocation, webviewReady, MACHINE_VIEW, DIRECTORY_MAP, SCOPE, ProjectStructureResponse, ProjectStructureArtifactResponse, CodeData, ProjectDiagnosticsResponse, Type } from "@wso2/ballerina-core"; +import { EVENT_TYPE, SyntaxTree, History, MachineStateValue, IUndoRedoManager, VisualizerLocation, webviewReady, MACHINE_VIEW, DIRECTORY_MAP, SCOPE, ProjectStructureResponse, ProjectStructureArtifactResponse, CodeData, ProjectDiagnosticsResponse, Type, dependencyPullProgress } from "@wso2/ballerina-core"; import { fetchAndCacheLibraryData } from './features/library-browser'; import { VisualizerWebview } from './views/visualizer/webview'; -import { commands, extensions, ShellExecution, Task, TaskDefinition, tasks, Uri, window, workspace, WorkspaceFolder } from 'vscode'; +import { commands, extensions, Uri, window, workspace, WorkspaceFolder } from 'vscode'; import { notifyCurrentWebview, RPCLayer } from './RPCLayer'; import { generateUid, getComponentIdentifier, getNodeByIndex, getNodeByName, getNodeByUid, getView } from './utils/state-machine-utils'; import * as path from 'path'; @@ -24,6 +24,8 @@ import { getWorkspaceTomlValues } from './utils'; import { buildProjectArtifactsStructure } from './utils/project-artifacts'; +import { runCommandWithOutput } from './utils/runCommand'; +import { buildOutputChannel } from './utils/logger'; export interface ProjectMetadata { readonly isBI: boolean; @@ -427,11 +429,7 @@ const stateMachine = createMachine( return; } - const taskDefinition: TaskDefinition = { - type: 'shell', - task: 'run' - }; - + // Construct the build command let buildCommand = 'bal build'; const config = workspace.getConfiguration('ballerina'); @@ -440,44 +438,36 @@ const stateMachine = createMachine( buildCommand = path.join(ballerinaHome, 'bin', buildCommand); } - // Use the current process environment which should have the updated PATH - const execution = new ShellExecution(buildCommand, { env: process.env as { [key: string]: string } }); - - if (!workspace.workspaceFolders || workspace.workspaceFolders.length === 0) { - resolve(true); - return; - } - - - const task = new Task( - taskDefinition, - workspace.workspaceFolders![0], - 'Ballerina Build', - 'ballerina', - execution - ); - try { - const taskExecution = await tasks.executeTask(task); - - // Wait for task completion - await new Promise((taskResolve) => { - // Listen for task completion - const disposable = tasks.onDidEndTask((taskEndEvent) => { - if (taskEndEvent.execution === taskExecution) { - console.log('Build task completed'); - - // Close the terminal pane on completion - commands.executeCommand('workbench.action.closePanel'); + // Execute the build command with output streaming + const result = await runCommandWithOutput( + buildCommand, + context.projectPath, + buildOutputChannel, + (message: string) => { + // Send progress notification to the visualizer + RPCLayer._messenger.sendNotification( + dependencyPullProgress, + { type: 'webview', webviewType: VisualizerWebview.viewType }, + message + ); + } + ); - disposable.dispose(); - taskResolve(); - } - }); - }); + if (result.success) { + console.log('Build task completed successfully'); + // Close the output panel on successful completion + commands.executeCommand('workbench.action.closePanel'); + } else { + const errorMsg = `Failed to build Ballerina package. Exit code: ${result.exitCode}`; + console.error(errorMsg); + window.showErrorMessage(errorMsg); + } } catch (error) { - window.showErrorMessage(`Failed to build Ballerina package: ${error}`); + const errorMsg = `Failed to build Ballerina package: ${error}`; + console.error(errorMsg, error); + window.showErrorMessage(errorMsg); } } diff --git a/workspaces/ballerina/ballerina-extension/src/utils/logger.ts b/workspaces/ballerina/ballerina-extension/src/utils/logger.ts index cc8be1a259a..d507c28b191 100644 --- a/workspaces/ballerina/ballerina-extension/src/utils/logger.ts +++ b/workspaces/ballerina/ballerina-extension/src/utils/logger.ts @@ -23,6 +23,7 @@ import os from 'os'; import fs from 'fs'; export const outputChannel = vscode.window.createOutputChannel("Ballerina"); +export const buildOutputChannel = vscode.window.createOutputChannel("Ballerina Build"); const logLevelDebug: boolean = getPluginConfig().get('debugLog') === true; function withNewLine(value: string) { diff --git a/workspaces/ballerina/ballerina-extension/src/utils/runCommand.ts b/workspaces/ballerina/ballerina-extension/src/utils/runCommand.ts index 49dddf0f045..ee791f5075e 100644 --- a/workspaces/ballerina/ballerina-extension/src/utils/runCommand.ts +++ b/workspaces/ballerina/ballerina-extension/src/utils/runCommand.ts @@ -38,6 +38,93 @@ export async function runBackgroundTerminalCommand(command: string) { }); } +/** + * Run a command with output streamed to an output channel + * @param command Command to execute + * @param cwd Working directory for the command + * @param outputChannel VSCode output channel to stream output to + * @param onProgress Optional callback to report progress (e.g., module being pulled) + * @returns Promise that resolves with success status and exit code + */ +export function runCommandWithOutput( + command: string, + cwd: string, + outputChannel: vscode.OutputChannel, + onProgress?: (message: string) => void +): Promise<{ success: boolean; exitCode: number | null }> { + return new Promise((resolve) => { + console.log(`[runCommandWithOutput] Executing: ${command} in ${cwd}`); + + // Show the output channel + outputChannel.show(true); + outputChannel.appendLine(`Running: ${command}`); + outputChannel.appendLine(`Working directory: ${cwd}`); + outputChannel.appendLine(''); + + // Spawn the process + const proc = child_process.spawn(command, [], { + shell: true, + cwd: cwd, + env: process.env as { [key: string]: string } + }); + + // Handle stdout + proc.stdout?.on('data', (data: Buffer) => { + const text = data.toString(); + outputChannel.append(text); + + // Parse module names from output for progress reporting + if (onProgress) { + // Look for patterns like "ballerinax/redis:3.1.0 [central.ballerina.io..." + const moduleMatch = text.match(/(\w+\/[\w-]+:\d+\.\d+\.\d+)\s+\[/); + if (moduleMatch) { + const moduleName = moduleMatch[1]; + onProgress(`Pulling ${moduleName} ...`); + } + + // Also check for "pulled from central successfully" messages + const successMatch = text.match(/(\w+\/[\w-]+:\d+\.\d+\.\d+)\s+pulled from central successfully/); + if (successMatch) { + const moduleName = successMatch[1]; + onProgress(`${moduleName} pulled successfully`); + } + } + }); + + // Handle stderr + proc.stderr?.on('data', (data: Buffer) => { + const text = data.toString(); + outputChannel.append(text); + console.log(`[runCommandWithOutput] stderr: ${text}`); + onProgress(`Something went wrong. check the output for more details.`); + }); + + // Handle process errors + proc.on('error', (error) => { + const errorMsg = `Process error: ${error.message}`; + outputChannel.appendLine(errorMsg); + console.error(`[runCommandWithOutput] ${errorMsg}`, error); + onProgress(`Something went wrong. check the output for more details.`); + resolve({ success: false, exitCode: null }); + }); + + // Handle process exit + proc.on('close', (code) => { + const exitMsg = `\nProcess exited with code ${code}`; + outputChannel.appendLine(exitMsg); + console.log(`[runCommandWithOutput] ${exitMsg}`); + + const success = code === 0; + if (success) { + onProgress(`All dependencies pulled successfully`); + } else { + onProgress(`Something went wrong. check the output for more details.`); + } + resolve({ success, exitCode: code }); + }); + }); +} + export function openExternalUrl(url:string){ vscode.env.openExternal(vscode.Uri.parse(url)); } diff --git a/workspaces/ballerina/ballerina-extension/src/views/visualizer/webview.ts b/workspaces/ballerina/ballerina-extension/src/views/visualizer/webview.ts index a7ed431ac61..95a8d941372 100644 --- a/workspaces/ballerina/ballerina-extension/src/views/visualizer/webview.ts +++ b/workspaces/ballerina/ballerina-extension/src/views/visualizer/webview.ts @@ -172,16 +172,15 @@ export class VisualizerWebview { .loader-wrapper { display: flex; justify-content: center; - align-items: flex-start; - height: 100%; + align-items: center; + height: 100vh; width: 100%; - padding-top: 30vh; } .loader { - width: 36px; + width: 28px; aspect-ratio: 1; border-radius: 50%; - border: 6px solid var(--vscode-button-background); + border: 5px solid var(--vscode-progressBar-background); animation: l20-1 0.5s infinite linear alternate, l20-2 1s infinite linear; @@ -210,13 +209,12 @@ export class VisualizerWebview { font-family: var(--vscode-font-family); } .logo-container { - margin-bottom: 2rem; display: flex; justify-content: center; } .welcome-title { color: var(--vscode-foreground); - margin: 0 0 0.5rem 0; + margin: 1.5rem 0 0.5rem 0; letter-spacing: -0.02em; font-size: 1.5em; font-weight: 400; @@ -229,7 +227,7 @@ export class VisualizerWebview { opacity: 0.8; } .loading-text { - color: var(--vscode-foreground); + color: var(--vscode-button-background); font-size: 13px; font-weight: 500; } diff --git a/workspaces/ballerina/ballerina-rpc-client/src/BallerinaRpcClient.ts b/workspaces/ballerina/ballerina-rpc-client/src/BallerinaRpcClient.ts index 0ad25d5aaa1..f8b7bdc0e62 100644 --- a/workspaces/ballerina/ballerina-rpc-client/src/BallerinaRpcClient.ts +++ b/workspaces/ballerina/ballerina-rpc-client/src/BallerinaRpcClient.ts @@ -51,7 +51,8 @@ import { currentThemeChanged, ChatNotify, onChatNotify, - AIMachineSendableEvent + AIMachineSendableEvent, + dependencyPullProgress } from "@wso2/ballerina-core"; import { LangClientRpcClient } from "./rpc-clients/lang-client/rpc-client"; import { LibraryBrowserRpcClient } from "./rpc-clients/library-browser/rpc-client"; @@ -237,6 +238,10 @@ export class BallerinaRpcClient { this.messenger.onNotification(onMigrationToolStateChanged, callback); } + onDependencyPullProgress(callback: (message: string) => void) { + this.messenger.onNotification(dependencyPullProgress, callback); + } + getPopupVisualizerState(): Promise { return this.messenger.sendRequest(getPopupVisualizerState, HOST_EXTENSION); } diff --git a/workspaces/ballerina/ballerina-visualizer/src/Visualizer.tsx b/workspaces/ballerina/ballerina-visualizer/src/Visualizer.tsx index bac5d61914b..a3cf5b283dd 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/Visualizer.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/Visualizer.tsx @@ -21,29 +21,26 @@ import { useRpcContext } from "@wso2/ballerina-rpc-client"; import { AIMachineStateValue, MachineStateValue } from "@wso2/ballerina-core"; import MainPanel from "./MainPanel"; import styled from '@emotion/styled'; -import { LoadingRing } from "./components/Loader"; import AIPanel from "./views/AIPanel/AIPanel"; import { AgentChat } from "./views/AgentChatPanel/AgentChat"; import { VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"; import { Global, css } from '@emotion/react'; +import { DownloadIcon } from "./components/DownloadIcon"; +import { ThemeColors } from "@wso2/ui-toolkit"; const ProgressRing = styled(VSCodeProgressRing)` - height: 50px; - width: 50px; - margin: 1.5rem; + height: 36px; + width: 36px; `; const LoadingContent = styled.div` display: flex; flex-direction: column; - justify-content: flex-start; + justify-content: center; align-items: center; - height: 100%; + height: 100vh; width: 100%; - padding-top: 30vh; text-align: center; - max-width: 500px; - margin: 0 auto; animation: fadeIn 1s ease-in-out; `; @@ -52,6 +49,7 @@ const LoadingTitle = styled.h1` font-size: 1.5em; font-weight: 400; margin: 0; + margin-top: 1.5rem; letter-spacing: -0.02em; line-height: normal; `; @@ -64,7 +62,7 @@ const LoadingSubtitle = styled.p` `; const LoadingText = styled.div` - color: var(--vscode-foreground); + color: ${ThemeColors.PRIMARY}; font-size: 13px; font-weight: 500; `; @@ -159,7 +157,7 @@ const LanguageServerLoadingView = () => { Activating Language Server - Preparing your Ballerina development environment + Preparing your Ballerina development environment. Initializing @@ -170,6 +168,15 @@ const LanguageServerLoadingView = () => { }; const PullingDependenciesView = () => { + const { rpcClient } = useRpcContext(); + const [currentModule, setCurrentModule] = React.useState('Compiling project...'); + + React.useEffect(() => { + rpcClient?.onDependencyPullProgress((message: string) => { + setCurrentModule(message); + }); + }, [rpcClient]); + return (
{ }}> - + Pulling Dependencies - Fetching required modules for your project.
- Please wait, this might take some time. + Please wait while your project dependencies are being pulled.
- Pulling + {currentModule}
diff --git a/workspaces/ballerina/ballerina-visualizer/src/components/DownloadIcon/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/components/DownloadIcon/index.tsx index 284050cffb8..d62c91822e3 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/components/DownloadIcon/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/components/DownloadIcon/index.tsx @@ -17,12 +17,13 @@ */ interface DownloadIconProps { - color?: string; + color?: string; + sx?: React.CSSProperties; } -export const DownloadIcon = ({ color = "#000" }: DownloadIconProps) => { +export const DownloadIcon = ({ color = "#000", sx }: DownloadIconProps) => { return ( - + diff --git a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/AddConnectionWizard/index.tsx b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/AddConnectionWizard/index.tsx index 0d39a7d3909..8c54e523c7c 100644 --- a/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/AddConnectionWizard/index.tsx +++ b/workspaces/ballerina/ballerina-visualizer/src/views/BI/Connection/AddConnectionWizard/index.tsx @@ -421,9 +421,9 @@ export function AddConnectionWizard(props: AddConnectionWizardProps) { )} {pullingStatus === PullingStatus.PULLING && ( - + - Please wait while the connector package is being pulled... + Please wait while the connector is being pulled. )} @@ -438,7 +438,7 @@ export function AddConnectionWizard(props: AddConnectionWizardProps) { height: "28px", }} /> - Connector package pulled successfully. + Connector pulled successfully. )} {pullingStatus === PullingStatus.ERROR && ( @@ -453,7 +453,7 @@ export function AddConnectionWizard(props: AddConnectionWizardProps) { }} /> - Failed to pull the connector package. Please try again. + Failed to pull the connector. Please try again. )} From 08a1b792260388f4556c008963cebb881fc46268 Mon Sep 17 00:00:00 2001 From: Kanushka Gayan Date: Wed, 12 Nov 2025 01:28:11 +0530 Subject: [PATCH 012/230] Implement temporary solution for resolving missing dependencies post-build - Added logic to retry resolving missing dependencies after a successful build. - Introduced notifications to inform when the project is reloaded successfully. - Implemented a delay to ensure the Overview component is ready before sending notifications. --- .../ballerina-extension/src/stateMachine.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/workspaces/ballerina/ballerina-extension/src/stateMachine.ts b/workspaces/ballerina/ballerina-extension/src/stateMachine.ts index 98c607b5057..eaa3ce5d4f9 100644 --- a/workspaces/ballerina/ballerina-extension/src/stateMachine.ts +++ b/workspaces/ballerina/ballerina-extension/src/stateMachine.ts @@ -456,6 +456,30 @@ const stateMachine = createMachine( if (result.success) { console.log('Build task completed successfully'); + + // HACK: Retry resolving missing dependencies after build is successful. This is a temporary solution to ensure the project is reloaded with new dependencies. + const projectUri = Uri.file(context.projectPath).toString(); + const dependenciesResponse = await StateMachine.langClient().resolveMissingDependencies({ + documentIdentifier: { + uri: projectUri + } + }); + + const response = dependenciesResponse as SyntaxTree; + if (response.parseSuccess) { + console.log('Project reloaded successfully'); + // Wait for the state to transition to viewReady, then notify + // This ensures Overview component is mounted before receiving the notification + setTimeout(() => { + if (StateMachine.isReady()) { + console.log('Notifying Overview after dependencies resolved'); + notifyCurrentWebview(); + } + }, 2000); // HACK: Fix this overview notification after dependencies resolved. + } else { + console.error('Failed to reload project'); + } + // Close the output panel on successful completion commands.executeCommand('workbench.action.closePanel'); } else { From 3d5b104f673913da071e7a08c636af47496fb2e9 Mon Sep 17 00:00:00 2001 From: Senith Uthsara Date: Wed, 12 Nov 2025 13:56:13 +0530 Subject: [PATCH 013/230] add code mirror basic functions --- common/config/rush/pnpm-lock.yaml | 17 +- .../ballerina-side-panel/package.json | 5 +- .../components/editors/ExpressionEditor.tsx | 2 +- .../components/editors/ExpressionField.tsx | 3 +- .../ChipExpressionBaseComponent.tsx | 23 +- .../ChipExpressionEditor/CodeUtils.ts | 272 ++++++++++++++++++ .../ChipExpressionBaseComponent2.tsx | 139 +++++++++ .../ChipExpressionEditor/utils.ts | 89 ++++-- 8 files changed, 512 insertions(+), 38 deletions(-) create mode 100644 workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/CodeUtils.ts create mode 100644 workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/components/ChipExpressionBaseComponent2.tsx diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 2de699ac7a9..3774f59c472 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -974,6 +974,15 @@ importers: ../../workspaces/ballerina/ballerina-side-panel: dependencies: + '@codemirror/commands': + specifier: ~6.10.0 + version: 6.10.0 + '@codemirror/state': + specifier: ~6.5.2 + version: 6.5.2 + '@codemirror/view': + specifier: ~6.38.6 + version: 6.38.6 '@emotion/react': specifier: ^11.14.0 version: 11.14.0(@types/react@18.2.0)(react@18.2.0) @@ -37332,8 +37341,8 @@ snapshots: '@webpack-cli/configtest@3.0.1(webpack-cli@6.0.1)(webpack@5.102.1)': dependencies: - webpack: 5.102.1(webpack-cli@6.0.1) - webpack-cli: 6.0.1(webpack@5.102.1) + webpack: 5.102.1(@swc/core@1.15.0(@swc/helpers@0.5.17))(webpack-cli@6.0.1) + webpack-cli: 6.0.1(webpack-dev-server@5.2.2)(webpack@5.102.1) '@webpack-cli/info@1.5.0(webpack-cli@4.10.0)': dependencies: @@ -37347,8 +37356,8 @@ snapshots: '@webpack-cli/info@3.0.1(webpack-cli@6.0.1)(webpack@5.102.1)': dependencies: - webpack: 5.102.1(webpack-cli@6.0.1) - webpack-cli: 6.0.1(webpack@5.102.1) + webpack: 5.102.1(@swc/core@1.15.0(@swc/helpers@0.5.17))(webpack-cli@6.0.1) + webpack-cli: 6.0.1(webpack-dev-server@5.2.2)(webpack@5.102.1) '@webpack-cli/serve@1.7.0(webpack-cli@4.10.0)': dependencies: diff --git a/workspaces/ballerina/ballerina-side-panel/package.json b/workspaces/ballerina/ballerina-side-panel/package.json index 8fcaa618b85..0c0913b52b5 100644 --- a/workspaces/ballerina/ballerina-side-panel/package.json +++ b/workspaces/ballerina/ballerina-side-panel/package.json @@ -28,7 +28,10 @@ "lodash": "~4.17.21", "react-hook-form": "7.56.4", "react-markdown": "~10.1.0", - "@github/markdown-toolbar-element": "^2.2.3" + "@github/markdown-toolbar-element": "^2.2.3", + "@codemirror/commands": "~6.10.0", + "@codemirror/state": "~6.5.2", + "@codemirror/view": "~6.38.6" }, "devDependencies": { "@storybook/react": "^6.5.16", diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpressionEditor.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpressionEditor.tsx index e609db98535..4e3a6e6dd52 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpressionEditor.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpressionEditor.tsx @@ -525,7 +525,7 @@ export const ExpressionEditor = (props: ExpressionEditorProps) => { }; // Only allow opening expanded mode for specific fields - const onOpenExpandedMode = (!props.isInExpandedMode && ["query", "instructions", "role"].includes(field.key)) + const onOpenExpandedMode = (!props.isInExpandedMode && ["query", "instructions", "role"].includes(field.key)) || inputMode === InputMode.EXP ? handleOpenExpandedMode : undefined; diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpressionField.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpressionField.tsx index ff6e22e907d..d535501312a 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpressionField.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpressionField.tsx @@ -31,6 +31,7 @@ import { InputMode } from './MultiModeExpressionEditor/ChipExpressionEditor/type import { ChipExpressionBaseComponent } from './MultiModeExpressionEditor/ChipExpressionEditor/ChipExpressionBaseComponent'; import { LineRange } from '@wso2/ballerina-core/lib/interfaces/common'; import { HelperpaneOnChangeOptions } from '../Form/types'; +import { ChipExpressionBaseComponent2 } from './MultiModeExpressionEditor/ChipExpressionEditor/components/ChipExpressionBaseComponent2'; export interface ExpressionField { inputMode: InputMode; @@ -150,7 +151,7 @@ export const ExpressionField: React.FC = ({ } return ( - void; @@ -97,13 +98,15 @@ export const ChipExpressionBaseComponent = (props: ChipExpressionBaseComponentPr const fetchUpdatedFilteredTokens = useCallback(async (value: string): Promise => { setIsLoading(true); try { - const response = await expressionEditorRpcManager?.getExpressionTokens( - value, - props.fileName, - props.targetLineRange.startLine - ); - setIsLoading(false); - return response || []; + // const response = await expressionEditorRpcManager?.getExpressionTokens( + // value, + // props.fileName, + // props.targetLineRange.startLine + // ); + // setIsLoading(false); + return new Promise((resolve) => { + resolve([]); + }); } catch (error) { setIsLoading(false); return []; @@ -517,7 +520,6 @@ export const ChipExpressionBaseComponent = (props: ChipExpressionBaseComponentPr {!props.isInExpandedMode && }
- {isLoading && } - + /> */} +
diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/CodeUtils.ts b/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/CodeUtils.ts new file mode 100644 index 00000000000..13262290fe1 --- /dev/null +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/CodeUtils.ts @@ -0,0 +1,272 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { StateEffect, StateField, RangeSet, Transaction } from "@codemirror/state"; +import { WidgetType, Decoration, ViewPlugin, EditorView, ViewUpdate, keymap } from "@codemirror/view"; +import { ParsedToken, filterCompletionsByPrefixAndType, getParsedExpressionTokens, getWordBeforeCursor, getWordBeforeCursorPosition } from "./utils"; +import { defaultKeymap, historyKeymap } from "@codemirror/commands"; +import { CompletionItem } from "@wso2/ui-toolkit"; + +export type TokenStream = number[]; + +export function createChip(text: string) { + class ChipWidget extends WidgetType { + constructor(readonly text: string) { + super(); + } + toDOM() { + const span = document.createElement("span"); + span.textContent = this.text; + span.style.background = "#007bff"; + span.style.color = "white"; + span.style.borderRadius = "12px"; + span.style.padding = "2px 8px"; + span.style.margin = "0 2px"; + span.style.display = "inline-block"; + span.style.cursor = "pointer"; + return span; + } + ignoreEvent() { + return true; + } + eq(other: ChipWidget) { + return other.text === this.text; + } + } + return Decoration.replace({ + widget: new ChipWidget(text), + inclusive: false, + block: false + }); +} + +export const chipTheme = EditorView.theme({ + ".cm-content": { + caretColor: "#ffffff" + }, + "&.cm-editor .cm-cursor, &.cm-editor .cm-dropCursor": { + borderLeftColor: "#ffffff" + } + +}); + +export const tokensChangeEffect = StateEffect.define(); +export const removeChipEffect = StateEffect.define(); // contains token ID + +export const tokenField = StateField.define({ + create() { + return []; + }, + update(oldTokens, tr) { + + oldTokens = oldTokens.map(token => ({ + ...token, + start: tr.changes.mapPos(token.start, 1), + end: tr.changes.mapPos(token.end, -1) + })); + + for (let effect of tr.effects) { + if (effect.is(tokensChangeEffect)) { + const tokenObjects = getParsedExpressionTokens(effect.value, tr.newDoc.toString()); + return tokenObjects; + } + if (effect.is(removeChipEffect)) { + const removingTokenId = effect.value; + return oldTokens.filter(token => token.id !== removingTokenId); + } + } + return oldTokens; + } +}); + +export const chipPlugin = ViewPlugin.fromClass( + class { + decorations: RangeSet; + constructor(view: EditorView) { + this.decorations = this.buildDecorations(view); + } + update(update: ViewUpdate) { + const hasTokensChangeEffect = update.transactions.some(tr => + tr.effects.some(e => e.is(tokensChangeEffect)) + ); + const hasDocOrViewportChange = update.docChanged || update.viewportChanged; + if (hasDocOrViewportChange || hasTokensChangeEffect) { + this.decorations = this.buildDecorations(update.view); + } + } + buildDecorations(view: EditorView) { + const widgets = []; + const tokens = view.state.field(tokenField); + + for (const token of tokens) { + const text = view.state.doc.sliceString(token.start, token.end); + widgets.push(createChip(text).range(token.start, token.end)); + } + + return Decoration.set(widgets, true); + } + }, + { + decorations: v => v.decorations + } +); + +export const expressionEditorKeymap = keymap.of([ + { + key: "Backspace", + run: (view) => { + const state = view.state; + const tokens = state.field(tokenField, false); + if (!tokens) return false; + + const cursor = state.selection.main.head; + + const affectedToken = tokens.find(token => token.start < cursor && token.end >= cursor); + + if (affectedToken) { + view.dispatch({ + effects: removeChipEffect.of(affectedToken.id), + changes: { from: affectedToken.start, to: affectedToken.end, insert: '' } + }); + return true; + } + return false; + } + }, + ...defaultKeymap, + ...historyKeymap +]); + +export const shouldOpenCompletionsListner = (onTrigger: (state: boolean, top: number, left: number, filteredCompletions: CompletionItem[]) => void, completions: CompletionItem[]) => { + const shouldOpenCompletionsListner = EditorView.updateListener.of((update) => { + const cursorPosition = update.view.state.selection.main.head; + const currentValue = update.view.state.doc.toString(); + const textBeforeCursor = currentValue.slice(0, cursorPosition); + + const wordBeforeCursor = getWordBeforeCursorPosition(textBeforeCursor); + if (update.docChanged && wordBeforeCursor.length > 0) { + const coords = update.view.coordsAtPos(cursorPosition); + if (coords && coords.top && coords.left) { + const newFilteredCompletions = filterCompletionsByPrefixAndType(completions, wordBeforeCursor); + onTrigger(true, coords.top, coords.left, newFilteredCompletions); + } + } + }) + + return shouldOpenCompletionsListner; +} + +export const shouldOpenHelperPaneState = (onTrigger: (state: boolean, top: number, left: number) => void) => { + const shouldOpenHelperPaneListner = EditorView.updateListener.of((update) => { + const cursorPosition = update.view.state.selection.main.head; + const currentValue = update.view.state.doc.toString(); + const textBeforeCursor = currentValue.slice(0, cursorPosition); + const triggerToken = textBeforeCursor.trimEnd().slice(-1); + const coords = update.view.coordsAtPos(cursorPosition); + + if (!update.view.hasFocus) { + onTrigger(false, 0, 0); + return; + } + if (coords && coords.top && coords.left && (update.view.hasFocus || triggerToken === '+' || triggerToken === ':')) { + const editorRect = update.view.dom.getBoundingClientRect(); + //+5 is to position a little be below the cursor + //otherwise it overlaps with the cursor + let relativeTop = coords.bottom - editorRect.top + 5; + let relativeLeft = coords.left - editorRect.left; + + const HELPER_PANE_WIDTH = 300; + const viewportWidth = window.innerWidth; + const absoluteLeft = coords.left; + const overflow = absoluteLeft + HELPER_PANE_WIDTH - viewportWidth; + + if (overflow > 0) { + relativeLeft -= overflow; + } + + onTrigger(true, relativeTop, relativeLeft); + } + }); + return shouldOpenHelperPaneListner; +}; + +export const buildNeedTokenRefetchListner = (onTrigger: () => void) => { + const needTokenRefetchListner = EditorView.updateListener.of((update) => { + const userEvent = update.transactions[0]?.annotation(Transaction.userEvent); + if (update.docChanged && ( + userEvent === "input.type" || + userEvent === "input.paste" || + userEvent === "delete.backward" || + userEvent === "delete.forward" || + userEvent === "delete.cut" + )) { + update.changes.iterChanges((_fromA, _toA, _fromB, _toB, inserted) => { + const insertedText = inserted.toString(); + if (insertedText.endsWith(' ')) { + onTrigger(); + } + }); + } + }); + return needTokenRefetchListner; +} + +export const buildOnChangeListner = (onTrigeer: (newValue: string, cursorPosition: number) => void) => { + const onChangeListner = EditorView.updateListener.of((update) => { + if (update.docChanged) { + const newValue = update.view.state.doc.toString(); + onTrigeer(newValue, update.view.state.selection.main.head); + } + }); + return onChangeListner; +} + +// export const cursorListener = EditorView.updateListener.of((update) => { +// if (update.selectionSet || update.docChanged) { +// console.log("Cursor or document changed"); +// } +// }); + +// export const focusOutListener = EditorView.updateListener.of((update) => { +// if (update.focusChanged && !update.view.hasFocus) { +// console.log("Editor lost focus"); +// } +// }); + +// export const focusInListener = EditorView.updateListener.of((update) => { +// if (update.focusChanged && update.view.hasFocus) { +// console.log("Editor gained focus"); +// } +// }); + +// export const onChangeListener = EditorView.updateListener.of((update) => { +// if (update.docChanged) { +// const newValue = update.view.state.doc.toString(); +// console.log("Document changed:", newValue); +// } +// }); + +// export const cursorPositionedAfterTriggerListener = EditorView.updateListener.of((update) => { +// if (update.selectionSet || update.docChanged) { +// const cursorPos = update.state.selection.main.head; +// const docText = update.state.doc.toString(); +// if (cursorPos > 0 && docText[cursorPos - 1] === '') { +// console.log("Cursor positioned after trigger character '#'"); +// } +// } +// }); diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/components/ChipExpressionBaseComponent2.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/components/ChipExpressionBaseComponent2.tsx new file mode 100644 index 00000000000..11622559836 --- /dev/null +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/components/ChipExpressionBaseComponent2.tsx @@ -0,0 +1,139 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { EditorState } from "@codemirror/state"; +import { EditorView } from "@codemirror/view"; +import React, { useEffect, useRef, useState } from "react"; +import { ChipExpressionBaseComponentProps } from "../ChipExpressionBaseComponent"; +import { useFormContext } from "../../../../../context"; +import { buildNeedTokenRefetchListner, buildOnChangeListner, chipPlugin, chipTheme, tokenField, tokensChangeEffect, expressionEditorKeymap, shouldOpenHelperPaneState, shouldOpenCompletionsListner } from "../CodeUtils"; +import { history } from "@codemirror/commands"; +import { ContextMenuContainer } from "../styles"; + +type ContextMenuType = "HELPER_PANE" | "COMPLETIONS"; + +type HelperPaneState = { + isOpen: boolean; + top: number; + left: number; + type: ContextMenuType; +} + +export const ChipExpressionBaseComponent2 = (props: ChipExpressionBaseComponentProps) => { + const [contextMenuState, setContextMenuState] = useState({ isOpen: false, top: 0, left: 0, type: "HELPER_PANE" as ContextMenuType }); + + const editorRef = useRef(null); + const viewRef = useRef(null); + const isTokenUpdateScheduled = useRef(true); + + const { expressionEditor } = useFormContext(); + const expressionEditorRpcManager = expressionEditor?.rpcManager; + + const needTokenRefetchListner = buildNeedTokenRefetchListner(() => { + isTokenUpdateScheduled.current = true; + }); + + const handleChangeListner = buildOnChangeListner((newValue, cursorPosition) => { + props.onChange(newValue, cursorPosition); + }); + + const handleHelperOpenListner = shouldOpenHelperPaneState((state, top, left) => { + setContextMenuState({ isOpen: state, top, left, type: "HELPER_PANE" }); + }); + + const handleCompletionsOpenListner = shouldOpenCompletionsListner((state, top, left) => { + setContextMenuState({ isOpen: state, top, left, type: "COMPLETIONS" }); + }, + props.completions + ); + + useEffect(() => { + if (!props.value || !viewRef.current) return; + const updateEditorState = async () => { + const currentDoc = viewRef.current!.state.doc.toString(); + if (currentDoc !== props.value) { + viewRef.current!.dispatch({ + changes: { from: 0, to: currentDoc.length, insert: props.value } + }); + } + + if (!isTokenUpdateScheduled.current) return; + const tokenStream = await expressionEditorRpcManager?.getExpressionTokens( + props.value, + props.fileName, + props.targetLineRange.startLine + ); + isTokenUpdateScheduled.current = false; + if (tokenStream) { + viewRef.current!.dispatch({ + effects: tokensChangeEffect.of(tokenStream) + }); + } + }; + updateEditorState(); + }, [props.value, props.fileName, props.targetLineRange.startLine]); + + useEffect(() => { + if (!editorRef.current) return; + const startState = EditorState.create({ + doc: props.value ?? "", + extensions: [ + history(), + expressionEditorKeymap, + chipPlugin, + tokenField, + chipTheme, + EditorView.lineWrapping, + needTokenRefetchListner, + handleChangeListner, + handleHelperOpenListner, + handleCompletionsOpenListner + ] + }); + const view = new EditorView({ + state: startState, + parent: editorRef.current + }); + viewRef.current = view; + return () => { + view.destroy(); + }; + }, []); + + return ( +
+
+ +
+ {contextMenuState.isOpen && + contextMenuState.type === "HELPER_PANE" && + ( + + {props.getHelperPane( + props.value, + () => { }, + "3/4" + )} + + )} +
+ ); +} diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/utils.ts b/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/utils.ts index 72a761ee39f..b03c4fb994f 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/utils.ts +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/utils.ts @@ -195,7 +195,7 @@ export const createExpressionModelFromTokens = ( if (!value) return []; if (!tokens || tokens.length === 0) { return [{ - ...expressionModelInitValue, value: value, length: value.length, + ...expressionModelInitValue, value: value, length: value.length, }]; } @@ -307,6 +307,8 @@ export const createExpressionModelFromTokens = ( return expressionModel; }; + + const getTokenTypeFromIndex = (index: number): string => { const tokenTypes: { [key: number]: string } = { 0: 'variable', @@ -351,9 +353,9 @@ export const getSelectionOffsets = (el: HTMLElement): { start: number; end: numb const offset = getCaretOffsetWithin(el); return { start: offset, end: offset }; } - + const range = selection.getRangeAt(0); - + if (!el.contains(range.startContainer) || !el.contains(range.endContainer)) { const offset = getCaretOffsetWithin(el); return { start: offset, end: offset }; @@ -365,13 +367,13 @@ export const getSelectionOffsets = (el: HTMLElement): { start: number; end: numb let startOffset = 0; let endOffset = 0; - + const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null); let current: Node | null = walker.nextNode(); - + while (current) { const textLength = (current.textContent || '').length; - + // Calculate start offset if (current === range.startContainer) { startOffset += range.startOffset; @@ -379,7 +381,7 @@ export const getSelectionOffsets = (el: HTMLElement): { start: number; end: numb // startContainer comes after current node startOffset += textLength; } - + // Calculate end offset if (current === range.endContainer) { endOffset += range.endOffset; @@ -387,7 +389,7 @@ export const getSelectionOffsets = (el: HTMLElement): { start: number; end: numb } else { endOffset += textLength; } - + current = walker.nextNode(); } @@ -1032,7 +1034,7 @@ const moveToPreviousElement = ( if (idx === i) { return { ...el, isFocused: true, focusOffsetStart: el.length, focusOffsetEnd: el.length }; } else if (idx === index) { - return { ...el, isFocused: false, focusOffsetStart: undefined, focusOffsetEnd: undefined}; + return { ...el, isFocused: false, focusOffsetStart: undefined, focusOffsetEnd: undefined }; } return el; }); @@ -1052,7 +1054,7 @@ const moveCaretBackward = ( ) => { const newExpressionModel = expressionModel.map((el, idx) => { if (idx === index) { - return { ...el, isFocused: true, focusOffsetStart: Math.max(0, caretOffset - 1), focusOffsetEnd: Math.max(0, caretOffset - 1)}; + return { ...el, isFocused: true, focusOffsetStart: Math.max(0, caretOffset - 1), focusOffsetEnd: Math.max(0, caretOffset - 1) }; } return el; }); @@ -1072,18 +1074,7 @@ export const getWordBeforeCursor = (expressionModel: ExpressionModel[]): string return fullTextUpToCursor.endsWith(lastMatch) ? lastMatch : ''; }; -export const filterCompletionsByPrefixAndType = (completions: CompletionItem[], prefix: string): CompletionItem[] => { - if (!prefix) { - return completions.filter(completion => - completion.kind === 'field' - ); - } - return completions.filter(completion => - (completion.kind === 'function' || completion.kind === 'variable' || completion.kind === 'field') && - completion.label.toLowerCase().startsWith(prefix.toLowerCase()) - ); -}; export const setCursorPositionToExpressionModel = (expressionModel: ExpressionModel[], cursorPosition: number): ExpressionModel[] => { const newExpressionModel = []; @@ -1213,3 +1204,59 @@ export const getModelWithModifiedParamsChip = (expressionModel: ExpressionModel[ export const isBetween = (a: number, b: number, target: number): boolean => { return target >= Math.min(a, b) && target <= Math.max(a, b); } + + + +//new + +export type ParsedToken = { + id:number; + start: number; + end: number; +} + +export const getParsedExpressionTokens = (tokens: number[], value: string) => { + const chunks = getTokenChunks(tokens); + let currentLine = 0; + let currentChar = 0; + const tokenObjects: ParsedToken[] = []; + + let tokenId = 0; + for (let chunk of chunks) { + const deltaLine = chunk[TOKEN_LINE_OFFSET_INDEX]; + const deltaStartChar = chunk[TOKEN_START_CHAR_OFFSET_INDEX]; + const length = chunk[TOKEN_LENGTH_INDEX]; + + currentLine += deltaLine; + if (deltaLine === 0) { + currentChar += deltaStartChar; + } else { + currentChar = deltaStartChar; + } + + const absoluteStart = getAbsoluteColumnOffset(value, currentLine, currentChar); + const absoluteEnd = absoluteStart + length; + + tokenObjects.push({ id: tokenId++, start: absoluteStart, end: absoluteEnd }); + } + return tokenObjects; +} + +export const getWordBeforeCursorPosition = (textBeforeCursor: string): string => { + const match = textBeforeCursor.match(/\b\w+$/); + const lastMatch = match ? match[match.length - 1] : ""; + return textBeforeCursor.endsWith(lastMatch) ? lastMatch : ''; +}; + +export const filterCompletionsByPrefixAndType = (completions: CompletionItem[], prefix: string): CompletionItem[] => { + if (!prefix) { + return completions.filter(completion => + completion.kind === 'field' + ); + } + + return completions.filter(completion => + (completion.kind === 'function' || completion.kind === 'variable' || completion.kind === 'field') && + completion.label.toLowerCase().startsWith(prefix.toLowerCase()) + ); +}; From 0f878d2bb7259c8b29468487b8403a477e2fd370 Mon Sep 17 00:00:00 2001 From: Dan Niles Date: Wed, 12 Nov 2025 13:59:30 +0530 Subject: [PATCH 014/230] Add expression mode to expaned editor --- .../editors/ExpandedEditor/ExpandedEditor.tsx | 47 ++++++++++++-- .../editors/ExpandedEditor/index.ts | 2 +- .../ExpandedEditor/modes/ExpressionMode.tsx | 63 +++++++++++++++++++ .../editors/ExpandedEditor/modes/types.ts | 34 +++++++++- .../components/editors/ExpressionEditor.tsx | 11 +++- .../components/AutoExpandingEditableDiv.tsx | 17 ++++- 6 files changed, 162 insertions(+), 12 deletions(-) create mode 100644 workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/modes/ExpressionMode.tsx diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/ExpandedEditor.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/ExpandedEditor.tsx index 85330799795..499c0fecc88 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/ExpandedEditor.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/ExpandedEditor.tsx @@ -19,12 +19,14 @@ import React, { useState, useEffect } from "react"; import { createPortal } from "react-dom"; import styled from "@emotion/styled"; -import { ThemeColors, Divider, Typography } from "@wso2/ui-toolkit"; -import { FormField } from "../../Form/types"; +import { ThemeColors, Divider, Typography, CompletionItem, FnSignatureDocumentation, HelperPaneHeight } from "@wso2/ui-toolkit"; +import { FormField, HelperpaneOnChangeOptions } from "../../Form/types"; import { EditorMode } from "./modes/types"; import { TextMode } from "./modes/TextMode"; import { PromptMode } from "./modes/PromptMode"; +import { ExpressionMode } from "./modes/ExpressionMode"; import { CompressButton } from "../MultiModeExpressionEditor/ChipExpressionEditor/components/FloatingButtonIcons"; +import { LineRange } from "@wso2/ballerina-core/lib/interfaces/common"; interface ExpandedPromptEditorProps { isOpen: boolean; @@ -32,6 +34,23 @@ interface ExpandedPromptEditorProps { value: string; onClose: () => void; onSave: (value: string) => void; + // Optional mode override (if not provided, will be auto-detected) + mode?: EditorMode; + // Expression mode specific props + completions?: CompletionItem[]; + fileName?: string; + targetLineRange?: LineRange; + extractArgsFromFunction?: (value: string, cursorPosition: number) => Promise<{ + label: string; + args: string[]; + currentArgIndex: number; + documentation?: FnSignatureDocumentation; + }>; + getHelperPane?: ( + value: string, + onChange: (value: string, options?: HelperpaneOnChangeOptions) => void, + helperPaneHeight: HelperPaneHeight + ) => React.ReactNode; } const ModalContainer = styled.div` @@ -89,7 +108,8 @@ const ModalContent = styled.div` */ const MODE_COMPONENTS: Record> = { text: TextMode, - prompt: PromptMode + prompt: PromptMode, + expression: ExpressionMode }; export const ExpandedEditor: React.FC = ({ @@ -98,10 +118,21 @@ export const ExpandedEditor: React.FC = ({ value, onClose, onSave, + mode: propMode, + completions, + fileName, + targetLineRange, + extractArgsFromFunction, + getHelperPane }) => { const [editedValue, setEditedValue] = useState(value); const promptFields = ["query", "instructions", "role"]; - const defaultMode: EditorMode = promptFields.includes(field.key) ? "prompt" : "text"; + + // Determine mode - use prop if provided, otherwise auto-detect + const defaultMode: EditorMode = propMode ?? ( + promptFields.includes(field.key) ? "prompt" : "text" + ); + const [mode] = useState(defaultMode); const [showPreview, setShowPreview] = useState(false); const [mouseDownTarget, setMouseDownTarget] = useState(null); @@ -147,6 +178,14 @@ export const ExpandedEditor: React.FC = ({ ...(mode === "prompt" && { isPreviewMode: showPreview, onTogglePreview: () => setShowPreview(!showPreview) + }), + // Props for expression mode + ...(mode === "expression" && { + completions, + fileName, + targetLineRange, + extractArgsFromFunction, + getHelperPane }) }; diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/index.ts b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/index.ts index 662f369f6ae..12d5a822011 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/index.ts +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/index.ts @@ -17,4 +17,4 @@ */ export { ExpandedEditor } from "./ExpandedEditor"; -export type { EditorMode, EditorModeProps, EditorModeWithPreviewProps } from "./modes/types"; +export type { EditorMode, EditorModeProps, EditorModeWithPreviewProps, EditorModeExpressionProps } from "./modes/types"; diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/modes/ExpressionMode.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/modes/ExpressionMode.tsx new file mode 100644 index 00000000000..cac8b006c89 --- /dev/null +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/modes/ExpressionMode.tsx @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from "react"; +import styled from "@emotion/styled"; +import { EditorModeExpressionProps } from "./types"; +import { ChipExpressionBaseComponent } from "../../MultiModeExpressionEditor/ChipExpressionEditor/ChipExpressionBaseComponent"; + +const ExpressionContainer = styled.div` + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + box-sizing: border-box; +`; + +/** + * Expression mode editor - uses ChipExpressionBaseComponent in expanded mode + */ +export const ExpressionMode: React.FC = ({ + value, + onChange, + completions = [], + fileName, + targetLineRange, + extractArgsFromFunction, + getHelperPane +}) => { + // Convert onChange signature from (value: string) => void to (value: string, cursorPosition: number) => void + const handleChange = (updatedValue: string, _updatedCursorPosition: number) => { + onChange(updatedValue); + }; + + return ( + + + + ); +}; diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/modes/types.ts b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/modes/types.ts index b2a812db9ff..c0588d973b8 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/modes/types.ts +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/modes/types.ts @@ -16,7 +16,9 @@ * under the License. */ -import { FormField } from "../../../Form/types"; +import { FormField, HelperpaneOnChangeOptions } from "../../../Form/types"; +import { CompletionItem, FnSignatureDocumentation, HelperPaneHeight } from "@wso2/ui-toolkit"; +import { LineRange } from "@wso2/ballerina-core/lib/interfaces/common"; /** * Base props that all editor mode components must implement @@ -40,15 +42,41 @@ export interface EditorModeWithPreviewProps extends EditorModeProps { onTogglePreview: (enabled: boolean) => void; } +/** + * Extended props for expression mode with completions and helper pane support + */ +export interface EditorModeExpressionProps extends EditorModeProps { + /** Completion items for autocomplete */ + completions?: CompletionItem[]; + /** File name for context */ + fileName?: string; + /** Target line range for context */ + targetLineRange?: LineRange; + /** Function to extract arguments from function calls */ + extractArgsFromFunction?: (value: string, cursorPosition: number) => Promise<{ + label: string; + args: string[]; + currentArgIndex: number; + documentation?: FnSignatureDocumentation; + }>; + /** Helper pane renderer function */ + getHelperPane?: ( + value: string, + onChange: (value: string, options?: HelperpaneOnChangeOptions) => void, + helperPaneHeight: HelperPaneHeight + ) => React.ReactNode; +} + /** * Mode type identifier */ -export type EditorMode = "text" | "prompt"; +export type EditorMode = "text" | "prompt" | "expression"; /** * Map of mode identifiers to their display labels */ export const MODE_LABELS: Record = { text: "Text", - prompt: "Prompt" + prompt: "Prompt", + expression: "Expression" }; diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpressionEditor.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpressionEditor.tsx index e609db98535..1a2e3f23c60 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpressionEditor.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpressionEditor.tsx @@ -524,8 +524,9 @@ export const ExpressionEditor = (props: ExpressionEditorProps) => { } }; - // Only allow opening expanded mode for specific fields - const onOpenExpandedMode = (!props.isInExpandedMode && ["query", "instructions", "role"].includes(field.key)) + // Only allow opening expanded mode for specific fields or expression mode + const onOpenExpandedMode = (!props.isInExpandedMode && + (["query", "instructions", "role"].includes(field.key) || inputMode === InputMode.EXP)) ? handleOpenExpandedMode : undefined; @@ -693,6 +694,12 @@ export const ExpressionEditor = (props: ExpressionEditorProps) => { value={watch(key)} onClose={() => setIsExpandedModalOpen(false)} onSave={handleSaveExpandedMode} + mode={inputMode === InputMode.EXP ? "expression" : undefined} + completions={completions} + fileName={effectiveFileName} + targetLineRange={effectiveTargetLineRange} + extractArgsFromFunction={handleExtractArgsFromFunction} + getHelperPane={handleGetHelperPane} /> )} diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/components/AutoExpandingEditableDiv.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/components/AutoExpandingEditableDiv.tsx index 44a035962c8..088ac8f0b8d 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/components/AutoExpandingEditableDiv.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/components/AutoExpandingEditableDiv.tsx @@ -22,7 +22,7 @@ import { CompletionItem, HelperPaneHeight } from "@wso2/ui-toolkit"; import { ContextMenuContainer, Completions, FloatingButtonContainer, COMPLETIONS_WIDTH } from "../styles"; import { CompletionsItem } from "./CompletionsItem"; import { FloatingToggleButton } from "./FloatingToggleButton"; -import { CloseHelperButton, OpenHelperButton } from "./FloatingButtonIcons"; +import { CloseHelperButton, OpenHelperButton, ExpandButton } from "./FloatingButtonIcons"; import { DATA_CHIP_ATTRIBUTE, DATA_ELEMENT_ID_ATTRIBUTE, ARIA_PRESSED_ATTRIBUTE, CHIP_MENU_VALUE, CHIP_TRUE_VALUE, EXPANDED_EDITOR_HEIGHT } from '../constants'; import { getCompletionsMenuPosition, isBetween } from "../utils"; import styled from "@emotion/styled"; @@ -37,7 +37,13 @@ const ChipEditorFieldContainer = styled.div` transition: opacity 0.2s ease-in-out; } - &:hover #floating-button-container { + #chip-expression-expand { + opacity: 0; + transition: opacity 0.2s ease-in-out; + } + + &:hover #floating-button-container, + &:hover #chip-expression-expand { opacity: 1; } `; @@ -290,6 +296,13 @@ export const AutoExpandingEditableDiv = (props: AutoExpandingEditableDivProps) = {renderCompletionsMenu()} {renderHelperPane()} + {props.onOpenExpandedMode && !props.isInExpandedMode && ( +
+ + + +
+ )} props.onToggleHelperPane?.()} title={props.isHelperPaneOpen ? "Close Helper" : "Open Helper"}> {props.isHelperPaneOpen ? : } From 1e297db9b00491475ffc99b3176e02e3c6a6ea36 Mon Sep 17 00:00:00 2001 From: Senith Uthsara Date: Wed, 12 Nov 2025 16:17:50 +0530 Subject: [PATCH 015/230] improve expanded editor for expression editors --- .../editors/ExpandedEditor/ExpandedEditor.tsx | 12 +- .../ExpandedEditor/modes/ExpressionMode.tsx | 4 +- .../ExpandedEditor/modes/PromptMode.tsx | 14 +-- .../editors/ExpandedEditor/modes/TextMode.tsx | 2 +- .../editors/ExpandedEditor/modes/types.ts | 2 +- .../components/editors/ExpressionEditor.tsx | 73 ++++++++--- .../ChipExpressionBaseComponent.tsx | 116 +++++++++++------- .../components/AutoExpandingEditableDiv.tsx | 22 ++-- .../ChipExpressionEditor/utils.ts | 4 +- .../src/components/editors/TextAreaEditor.tsx | 15 +-- .../Components/HelperPaneListItem.tsx | 8 ++ 11 files changed, 178 insertions(+), 94 deletions(-) diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/ExpandedEditor.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/ExpandedEditor.tsx index 499c0fecc88..2e92bcd49d7 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/ExpandedEditor.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/ExpandedEditor.tsx @@ -34,6 +34,7 @@ interface ExpandedPromptEditorProps { value: string; onClose: () => void; onSave: (value: string) => void; + onChange: (updatedValue: string, updatedCursorPosition: number) => void; // Optional mode override (if not provided, will be auto-detected) mode?: EditorMode; // Expression mode specific props @@ -117,6 +118,7 @@ export const ExpandedEditor: React.FC = ({ field, value, onClose, + onChange, onSave, mode: propMode, completions, @@ -125,7 +127,6 @@ export const ExpandedEditor: React.FC = ({ extractArgsFromFunction, getHelperPane }) => { - const [editedValue, setEditedValue] = useState(value); const promptFields = ["query", "instructions", "role"]; // Determine mode - use prop if provided, otherwise auto-detect @@ -137,10 +138,6 @@ export const ExpandedEditor: React.FC = ({ const [showPreview, setShowPreview] = useState(false); const [mouseDownTarget, setMouseDownTarget] = useState(null); - useEffect(() => { - setEditedValue(value); - }, [value, isOpen]); - useEffect(() => { if (mode === "text") { setShowPreview(false); @@ -148,7 +145,6 @@ export const ExpandedEditor: React.FC = ({ }, [mode]); const handleMinimize = () => { - onSave(editedValue); onClose(); }; @@ -171,8 +167,8 @@ export const ExpandedEditor: React.FC = ({ // Prepare props for the mode component const modeProps = { - value: editedValue, - onChange: setEditedValue, + value: value, + onChange: onChange, field, // Props for modes with preview support ...(mode === "prompt" && { diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/modes/ExpressionMode.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/modes/ExpressionMode.tsx index cac8b006c89..ee8954b1493 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/modes/ExpressionMode.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/modes/ExpressionMode.tsx @@ -42,8 +42,8 @@ export const ExpressionMode: React.FC = ({ getHelperPane }) => { // Convert onChange signature from (value: string) => void to (value: string, cursorPosition: number) => void - const handleChange = (updatedValue: string, _updatedCursorPosition: number) => { - onChange(updatedValue); + const handleChange = (updatedValue: string, updatedCursorPosition: number) => { + onChange(updatedValue, updatedCursorPosition); }; return ( diff --git a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/modes/PromptMode.tsx b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/modes/PromptMode.tsx index 6e0244bff2c..2bc061a6dda 100644 --- a/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/modes/PromptMode.tsx +++ b/workspaces/ballerina/ballerina-side-panel/src/components/editors/ExpandedEditor/modes/PromptMode.tsx @@ -77,7 +77,7 @@ export const PromptMode: React.FC = ({ if (!content.trim()) { e.preventDefault(); const newValue = textBeforeCursor.substring(0, lastNewlineIndex + 1) + '\n' + textAfterCursor; - onChange(newValue); + onChange(newValue, cursorPosition); // Set cursor position after both newlines queueMicrotask(() => { textarea.selectionStart = textarea.selectionEnd = lastNewlineIndex + 2; @@ -88,7 +88,7 @@ export const PromptMode: React.FC = ({ // Continue the list e.preventDefault(); const newValue = textBeforeCursor + '\n' + indent + marker + ' ' + textAfterCursor; - onChange(newValue); + onChange(newValue, cursorPosition); // Set cursor position after the list marker queueMicrotask(() => { const newCursorPos = cursorPosition + indent.length + marker.length + 2; @@ -106,7 +106,7 @@ export const PromptMode: React.FC = ({ if (!content.trim()) { e.preventDefault(); const newValue = textBeforeCursor.substring(0, lastNewlineIndex + 1) + '\n' + textAfterCursor; - onChange(newValue); + onChange(newValue, cursorPosition); // Set cursor position after both newlines queueMicrotask(() => { textarea.selectionStart = textarea.selectionEnd = lastNewlineIndex + 2; @@ -118,7 +118,7 @@ export const PromptMode: React.FC = ({ e.preventDefault(); const nextNumber = parseInt(number, 10) + 1; const newValue = textBeforeCursor + '\n' + indent + nextNumber + '. ' + textAfterCursor; - onChange(newValue); + onChange(newValue, cursorPosition); // Set cursor position after the list marker queueMicrotask(() => { const newCursorPos = cursorPosition + indent.length + nextNumber.toString().length + 3; @@ -136,7 +136,7 @@ export const PromptMode: React.FC = ({ if (!content.trim()) { e.preventDefault(); const newValue = textBeforeCursor.substring(0, lastNewlineIndex + 1) + '\n' + textAfterCursor; - onChange(newValue); + onChange(newValue, cursorPosition); // Set cursor position after both newlines queueMicrotask(() => { textarea.selectionStart = textarea.selectionEnd = lastNewlineIndex + 2; @@ -147,7 +147,7 @@ export const PromptMode: React.FC = ({ // Continue the task list with unchecked box e.preventDefault(); const newValue = textBeforeCursor + '\n' + indent + marker + ' [ ] ' + textAfterCursor; - onChange(newValue); + onChange(newValue, cursorPosition); // Set cursor position after the task marker queueMicrotask(() => { const newCursorPos = cursorPosition + indent.length + marker.length + 6; @@ -174,7 +174,7 @@ export const PromptMode: React.FC = ({