Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions common/config/rush/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ import {
FileOrDirRequest,
WorkspaceRootResponse,
ShowErrorMessageRequest,
WorkspaceTypeResponse
WorkspaceTypeResponse,
SampleDownloadRequest
} from "./interfaces";

export interface CommonRPCAPI {
Expand All @@ -51,4 +52,5 @@ export interface CommonRPCAPI {
showErrorMessage: (params: ShowErrorMessageRequest) => void;
getCurrentProjectTomlValues: () => Promise<Record<string, any>>;
getWorkspaceType: () => Promise<WorkspaceTypeResponse>;
downloadSelectedSampleFromGithub: (params: SampleDownloadRequest) => Promise<boolean>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,7 @@ export interface PackageTomlValues {
export interface WorkspaceTypeResponse {
type: "SINGLE_PROJECT" | "MULTIPLE_PROJECTS" | "BALLERINA_WORKSPACE" | "VSCODE_WORKSPACE" | "UNKNOWN"
}

export interface SampleDownloadRequest {
zipFileName: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ import {
FileOrDirRequest,
WorkspaceRootResponse,
ShowErrorMessageRequest,
WorkspaceTypeResponse
WorkspaceTypeResponse,
SampleDownloadRequest
} from "./interfaces";
import { RequestType, NotificationType } from "vscode-messenger-common";

Expand All @@ -53,3 +54,4 @@ export const getWorkspaceRoot: RequestType<void, WorkspaceRootResponse> = { meth
export const showErrorMessage: NotificationType<ShowErrorMessageRequest> = { method: `${_preFix}/showErrorMessage` };
export const getCurrentProjectTomlValues: RequestType<void, void> = { method: `${_preFix}/getCurrentProjectTomlValues` };
export const getWorkspaceType: RequestType<void, WorkspaceTypeResponse> = { method: `${_preFix}/getWorkspaceType` };
export const downloadSelectedSampleFromGithub: RequestType<SampleDownloadRequest, boolean> = { method: `${_preFix}/downloadSelectedSampleFromGithub` };
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ export enum MACHINE_VIEW {
AIAgentDesigner = "AI Agent Designer",
AIChatAgentWizard = "AI Chat Agent Wizard",
ResolveMissingDependencies = "Resolve Missing Dependencies",
ServiceFunctionForm = "Service Function Form"
ServiceFunctionForm = "Service Function Form",
BISamplesView = "BI Samples View"
}

export interface MachineEvent {
Expand Down
1 change: 1 addition & 0 deletions workspaces/ballerina/ballerina-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1214,6 +1214,7 @@
"portfinder": "^1.0.32",
"source-map-support": "^0.5.21",
"toml": "^3.0.0",
"unzipper": "~0.12.3",
"uuid": "^11.1.0",
"vscode-debugadapter": "^1.51.0",
"vscode-debugprotocol": "^1.51.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,29 @@
import {
BallerinaDiagnosticsRequest,
CommandsRequest,
FileOrDirRequest,
GoToSourceRequest,
OpenExternalUrlRequest,
RunExternalCommandRequest,
ShowErrorMessageRequest,
WorkspaceFileRequest,
downloadSelectedSampleFromGithub,
executeCommand,
experimentalEnabled,
FileOrDirRequest,
getBallerinaDiagnostics,
getCurrentProjectTomlValues,
getTypeCompletions,
getWorkspaceFiles,
getWorkspaceRoot,
getWorkspaceType,
goToSource,
GoToSourceRequest,
isNPSupported,
openExternalUrl,
OpenExternalUrlRequest,
runBackgroundTerminalCommand,
RunExternalCommandRequest,
SampleDownloadRequest,
selectFileOrDirPath,
selectFileOrFolderPath,
showErrorMessage
showErrorMessage,
ShowErrorMessageRequest,
WorkspaceFileRequest
} from "@wso2/ballerina-core";
import { Messenger } from "vscode-messenger";
import { CommonRpcManager } from "./rpc-manager";
Expand All @@ -62,4 +64,5 @@ export function registerCommonRpcHandlers(messenger: Messenger) {
messenger.onNotification(showErrorMessage, (args: ShowErrorMessageRequest) => rpcManger.showErrorMessage(args));
messenger.onRequest(getCurrentProjectTomlValues, () => rpcManger.getCurrentProjectTomlValues());
messenger.onRequest(getWorkspaceType, () => rpcManger.getWorkspaceType());
messenger.onRequest(downloadSelectedSampleFromGithub, (args: SampleDownloadRequest) => rpcManger.downloadSelectedSampleFromGithub(args));
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,24 @@ import {
FileOrDirResponse,
GoToSourceRequest,
OpenExternalUrlRequest,
PackageTomlValues,
RunExternalCommandRequest,
RunExternalCommandResponse,
SampleDownloadRequest,
ShowErrorMessageRequest,
SyntaxTree,
PackageTomlValues,
TypeResponse,
WorkspaceFileRequest,
WorkspaceRootResponse,
WorkspacesFileResponse,
WorkspaceTypeResponse,
WorkspaceTypeResponse
} from "@wso2/ballerina-core";
import child_process from 'child_process';
import { Uri, commands, env, window, workspace, MarkdownString } from "vscode";
import path from "path";
import os from "os";
import fs from "fs";
import * as unzipper from 'unzipper';
import { commands, env, MarkdownString, ProgressLocation, Uri, window, workspace } from "vscode";
import { URI } from "vscode-uri";
import { extension } from "../../BalExtensionContext";
import { StateMachine } from "../../stateMachine";
Expand All @@ -60,9 +65,11 @@ import {
askFilePath,
askProjectPath,
BALLERINA_INTEGRATOR_ISSUES_URL,
getUpdatedSource
getUpdatedSource,
handleDownloadFile,
selectSampleDownloadPath
} from "./utils";
import path from "path";
import { VisualizerWebview } from "../../views/visualizer/webview";

export class CommonRpcManager implements CommonRPCAPI {
async getTypeCompletions(): Promise<TypeResponse> {
Expand Down Expand Up @@ -304,4 +311,149 @@ export class CommonRpcManager implements CommonRPCAPI {

return { type: "UNKNOWN" };
}


async downloadSelectedSampleFromGithub(params: SampleDownloadRequest): Promise<boolean> {
const repoUrl = 'https://raw.githubusercontent.com/wso2/integration-samples/refs/heads/main/ballerina-integrator/samples/';
const rawFileLink = repoUrl + params.zipFileName + '.zip';
const defaultDownloadsPath = path.join(os.homedir(), 'Downloads'); // Construct the default downloads path
const pathFromDialog = await selectSampleDownloadPath();
if (pathFromDialog === "") {
return false;
}
const selectedPath = pathFromDialog === "" ? defaultDownloadsPath : pathFromDialog;
const filePath = path.join(selectedPath, params.zipFileName + '.zip');
let isSuccess = false;

if (fs.existsSync(filePath)) {
// already downloaded
isSuccess = true;
} else {
await window.withProgress({
location: ProgressLocation.Notification,
title: 'Downloading file',
cancellable: true
}, async (progress, cancellationToken) => {

let cancelled: boolean = false;
cancellationToken.onCancellationRequested(async () => {
cancelled = true;
// Clean up partial download
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
});

try {
await handleDownloadFile(rawFileLink, filePath, progress);
isSuccess = true;
return;
} catch (error) {
window.showErrorMessage(`Error while downloading the file: ${error}`);
}
});
}

if (isSuccess) {
const successMsg = `The Integration sample file has been downloaded successfully to the following directory: ${filePath}.`;
const zipReadStream = fs.createReadStream(filePath);
if (fs.existsSync(path.join(selectedPath, params.zipFileName))) {
// already extracted
let uri = Uri.file(path.join(selectedPath, params.zipFileName));
commands.executeCommand("vscode.openFolder", uri, true);
return true;
}

let extractionError: Error | null = null;
const parseStream = unzipper.Parse();

// Handle errors on the read stream
zipReadStream.on("error", (error) => {
extractionError = error;
window.showErrorMessage(`Failed to read zip file: ${error.message}`);
});

// Handle errors on the parse stream
parseStream.on("error", (error) => {
extractionError = error;
window.showErrorMessage(`Failed to parse zip file. The file may be corrupted: ${error.message}`);
});

parseStream.on("entry", function (entry) {
// Skip processing if we've already encountered an error
if (extractionError) {
entry.autodrain();
return;
}

var isDir = entry.type === "Directory";
var fullpath = path.join(selectedPath, entry.path);
var directory = isDir ? fullpath : path.dirname(fullpath);

try {
if (!fs.existsSync(directory)) {
fs.mkdirSync(directory, { recursive: true });
}
} catch (error) {
extractionError = error as Error;
window.showErrorMessage(`Failed to create directory "${directory}": ${error instanceof Error ? error.message : String(error)}`);
entry.autodrain();
return;
}

if (!isDir) {
const writeStream = fs.createWriteStream(fullpath);

// Handle write stream errors
writeStream.on("error", (error) => {
extractionError = error;
window.showErrorMessage(`Failed to write file "${fullpath}": ${error.message}. This may be due to insufficient disk space or permission issues.`);
entry.autodrain();
});

// Handle entry stream errors
entry.on("error", (error) => {
extractionError = error;
window.showErrorMessage(`Failed to extract entry "${entry.path}": ${error.message}`);
writeStream.destroy();
});

entry.pipe(writeStream);
}
});

parseStream.on("close", () => {
if (extractionError) {
console.error("Extraction failed:", extractionError);
window.showErrorMessage(`Sample extraction failed: ${extractionError.message}`);
return;
}

console.log("Extraction complete!");
window.showInformationMessage('Where would you like to open the project?',
{ modal: true },
'Current Window',
'New Window'
).then(selection => {
if (selection === "Current Window") {
// Dispose the current webview
VisualizerWebview.currentPanel?.dispose();
const folderUri = Uri.file(path.join(selectedPath, params.zipFileName));
const workspaceFolders = workspace.workspaceFolders || [];
if (!workspaceFolders.some(folder => folder.uri.fsPath === folderUri.fsPath)) {
workspace.updateWorkspaceFolders(workspaceFolders.length, 0, { uri: folderUri });
}
} else if (selection === "New Window") {
commands.executeCommand('vscode.openFolder', Uri.file(path.join(selectedPath, params.zipFileName)));
}
});
});

zipReadStream.pipe(parseStream);
window.showInformationMessage(
successMsg,
);
}
return isSuccess;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,18 @@

import * as os from 'os';
import { NodePosition } from "@wso2/syntax-tree";
import { Position, Range, Uri, window, workspace, WorkspaceEdit } from "vscode";
import { Position, Progress, Range, Uri, window, workspace, WorkspaceEdit } from "vscode";
import { TextEdit } from "@wso2/ballerina-core";
import axios from 'axios';
import fs from 'fs';

export const BALLERINA_INTEGRATOR_ISSUES_URL = "https://github.com/wso2/product-ballerina-integrator/issues";

interface ProgressMessage {
message: string;
increment?: number;
}

export function getUpdatedSource(
statement: string,
currentFileContent: string,
Expand Down Expand Up @@ -111,3 +118,75 @@ export async function applyBallerinaTomlEdit(tomlPath: Uri, textEdit: TextEdit)
}
});
}

export async function selectSampleDownloadPath(): Promise<string> {
const folderPath = await window.showOpenDialog({ title: 'Sample download directory', canSelectFolders: true, canSelectFiles: false, openLabel: 'Select Folder' });
if (folderPath && folderPath.length > 0) {
const newlySelectedFolder = folderPath[0].fsPath;
return newlySelectedFolder;
}
return "";
}

async function downloadFile(url: string, filePath: string, progressCallback?: (downloadProgress: any) => void) {
const writer = fs.createWriteStream(filePath);
let totalBytes = 0;
try {
const response = await axios.get(url, {
responseType: 'stream',
headers: {
"User-Agent": "Mozilla/5.0"
},
onDownloadProgress: (progressEvent) => {
totalBytes = progressEvent.total ?? 0;
if (totalBytes === 0) {
// Cannot calculate progress without total size
return;
}
const formatSize = (sizeInBytes: number) => {
const sizeInKB = sizeInBytes / 1024;
if (sizeInKB < 1024) {
return `${Math.floor(sizeInKB)} KB`;
} else {
return `${Math.floor(sizeInKB / 1024)} MB`;
}
};
const progress = {
percentage: Math.round((progressEvent.loaded * 100) / totalBytes),
downloadedAmount: formatSize(progressEvent.loaded),
downloadSize: formatSize(totalBytes)
};
if (progressCallback) {
progressCallback(progress);
}
}
});
response.data.pipe(writer);
await new Promise<void>((resolve, reject) => {
writer.on('finish', () => {
writer.close();
resolve();
});

writer.on('error', (error) => {
reject(error);
});
});
} catch (error) {
window.showErrorMessage(`Error while downloading the file: ${error}`);
throw error;
}
}

export async function handleDownloadFile(rawFileLink: string, defaultDownloadsPath: string, progress: Progress<ProgressMessage>) {
const handleProgress = (progressPercentage) => {
progress.report({ message: "Downloading file...", increment: progressPercentage });
};
try {
await downloadFile(rawFileLink, defaultDownloadsPath, handleProgress);
} catch (error) {
window.showErrorMessage(`Failed to download file: ${error}`);
}
progress.report({ message: "Download finished" });
}

Loading
Loading