Repository: microsoft/kiota (csharp)

Issue:
Kiota Workspace panel should list all clients and plugins available in the current VS code workspace
## Challenge
Due to time constraints, when we first implemented the workspace panel we added a reference to the workspace file so one could easily find it and open it to edit their existing clients and plugins.

However, the intention behind that panel was to list all clients and plugins in the current context.

## Proposal

We need to modify the existing workspace panel to list all clients and plugins available in the current context.

![Image](https://github.com/user-attachments/assets/15537880-b74d-40f7-8111-5b71fcf45290)

## Tasks
- [ ] Move the `My Workspace` panel to the top
- [ ] List all clients and plugins available in `workspace.json`
- [ ] Allow remove and edit for each client and plugin listed

## Acceptance Criteria
1. When there are clients and plugins in the current context, Kiota should show the Workspace panel expanded and listing all clients and plugins available as the image above.
2. The "My workspace" panel must be in the top.
3. When there is no clients or plugins in the context, the Workspace panel should load collapsed when opening Kiota extension.
4. When there is no clients or plugins in the context and one expands the Workspace panel, we should show a "No clients or plugins are available" in the panel.
5. Only show the `Clients` component when there is at least 1 client in the current context. If no client was found, the node shouldn't be displayed.
6. Only show the `Plugins` component when there is at least 1 plugin in the current context. If no plugin was found, the node shouldn't be displayed.
7. `Clients` and `Plugins` components must to be expandable/collapsible.
8. When hovering the `{ }` icon in any existing plugin/client, we should show the text `Select`. When clicking on it, the API Explorer should be loaded with the right client/plugin and right selected paths (same as the `EditPaths` codelense action) and the [notification](https://github.com/microsoft/kiota/issues/5712) for regeneration should be displayed.
10. When hovering the trash icon in any existing plugin/client, we should show the text `Delete`. When clicking on it for any existing plugin/client, the [plugin remove](https://github.com/microsoft/kiota/blob/main/specs/cli/plugin-remove.md) should be performed.

Pull request description:
### Overview
Closes #5715 

Also:
- I noticed that for items to load / refresh on the workspace tree, the workspace file needed to be open. This is no longer the case.
- When deleting a client/plugin that has been opened in the API Explorer, it is closed first then deleted to remove any chance for unparented explorer items

### Demo
![image](https://github.com/user-attachments/assets/cc67d5c9-1f39-430f-b928-ed69f765944b)

## Notes:
Can't fix _When there is no clients or plugins in the context, the Workspace panel should load collapsed when opening Kiota extension_ because VS Code does not provide a direct way to set the collapsed state of a view container through the package.json configuration. And programmatically changing it has not worked

Patch:
diff --git a/vscode/microsoft-kiota/src/commands/deleteWorkspaceItem/deleteWorkspaceItemCommand.ts b/vscode/microsoft-kiota/src/commands/deleteWorkspaceItem/deleteWorkspaceItemCommand.ts
index 8d84b15a3e..4eb9b79c05 100644
--- a/vscode/microsoft-kiota/src/commands/deleteWorkspaceItem/deleteWorkspaceItemCommand.ts
+++ b/vscode/microsoft-kiota/src/commands/deleteWorkspaceItem/deleteWorkspaceItemCommand.ts
@@ -3,6 +3,8 @@ import * as vscode from "vscode";
 
 import { extensionId } from "../../constants";
 import { getLogEntriesForLevel, KiotaLogEntry, LogLevel } from "../../kiotaInterop";
+import { OpenApiTreeProvider } from "../../providers/openApiTreeProvider";
+import { SharedService } from "../../providers/sharedService";
 import { WorkspaceTreeItem } from "../../providers/workspaceTreeProvider";
 import { isPluginType } from "../../util";
 import { exportLogsAndShowErrors } from "../../utilities/logging";
@@ -12,7 +14,9 @@ import { removeClient, removePlugin } from "./removeItem";
 export class DeleteWorkspaceItemCommand extends Command {
   constructor(
     private _context: vscode.ExtensionContext,
-    private _kiotaOutputChannel: vscode.LogOutputChannel
+    private _openApiTreeProvider: OpenApiTreeProvider,
+    private _kiotaOutputChannel: vscode.LogOutputChannel,
+    private sharedService: SharedService
   ) {
     super();
   }
@@ -33,9 +37,14 @@ export class DeleteWorkspaceItemCommand extends Command {
     );
 
     if (response?.title === yesAnswer.title) {
+      if (this.sharedService.get('clientOrPluginKey') === workspaceTreeItem.label) {
+        this._openApiTreeProvider.closeDescription();
+      }
+
       const result = await this.deleteItem(type, workspaceTreeItem);
       if (result) {
         const isSuccess = result.some(k => k.message.includes('removed successfully'));
+        await vscode.commands.executeCommand('kiota.workspace.refresh'); 
         if (isSuccess) {
           void vscode.window.showInformationMessage(vscode.l10n.t('{0} removed successfully.', workspaceTreeItem.label));
         } else {
diff --git a/vscode/microsoft-kiota/src/commands/generate/generateClientCommand.ts b/vscode/microsoft-kiota/src/commands/generate/generateClientCommand.ts
index 172f138fa6..c7e21c47f0 100644
--- a/vscode/microsoft-kiota/src/commands/generate/generateClientCommand.ts
+++ b/vscode/microsoft-kiota/src/commands/generate/generateClientCommand.ts
@@ -172,6 +172,7 @@ export class GenerateClientCommand extends Command {
         } else {
           await displayGenerationResults(this._openApiTreeProvider, config);
         }
+        await vscode.commands.executeCommand('kiota.workspace.refresh'); 
       }
 
       clearDeepLinkParams();  // Clear the state after successful generation
diff --git a/vscode/microsoft-kiota/src/commands/regenerate/regenerateButtonCommand.ts b/vscode/microsoft-kiota/src/commands/regenerate/regenerateButtonCommand.ts
index 44e07ad3ff..7319264b44 100644
--- a/vscode/microsoft-kiota/src/commands/regenerate/regenerateButtonCommand.ts
+++ b/vscode/microsoft-kiota/src/commands/regenerate/regenerateButtonCommand.ts
@@ -61,6 +61,7 @@ export class RegenerateButtonCommand extends Command {
         await regenerateService.regenerateTeamsApp(workspaceJson, clientOrPluginKey);
       }
     }
+    await vscode.commands.executeCommand('kiota.workspace.refresh');
   }
 
 }
\ No newline at end of file
diff --git a/vscode/microsoft-kiota/src/commands/regenerate/regenerateCommand.ts b/vscode/microsoft-kiota/src/commands/regenerate/regenerateCommand.ts
index d9647fe9b6..c4c0d673fc 100644
--- a/vscode/microsoft-kiota/src/commands/regenerate/regenerateCommand.ts
+++ b/vscode/microsoft-kiota/src/commands/regenerate/regenerateCommand.ts
@@ -46,5 +46,6 @@ export class RegenerateCommand extends Command {
         await regenerateService.regenerateTeamsApp(workspaceJson, clientOrPluginKey);
       }
     }
+    await vscode.commands.executeCommand('kiota.workspace.refresh'); 
   }
 }
\ No newline at end of file
diff --git a/vscode/microsoft-kiota/src/extension.ts b/vscode/microsoft-kiota/src/extension.ts
index a11d7471da..19e9828adc 100644
--- a/vscode/microsoft-kiota/src/extension.ts
+++ b/vscode/microsoft-kiota/src/extension.ts
@@ -82,7 +82,7 @@ export async function activate(
   const closeDescriptionCommand = new CloseDescriptionCommand(openApiTreeProvider);
   const statusCommand = new StatusCommand();
   const selectLockCommand = new SelectLockCommand(openApiTreeProvider);
-  const deleteWorkspaceItemCommand = new DeleteWorkspaceItemCommand(context, kiotaOutputChannel);
+  const deleteWorkspaceItemCommand = new DeleteWorkspaceItemCommand(context, openApiTreeProvider, kiotaOutputChannel, sharedService);
   const updateClientsCommand = new UpdateClientsCommand(context, kiotaOutputChannel);
 
   await loadTreeView(context, workspaceTreeProvider);
diff --git a/vscode/microsoft-kiota/src/modules/workspace/workspaceContentService.ts b/vscode/microsoft-kiota/src/modules/workspace/workspaceContentService.ts
index a7997b2444..1ea61a6882 100644
--- a/vscode/microsoft-kiota/src/modules/workspace/workspaceContentService.ts
+++ b/vscode/microsoft-kiota/src/modules/workspace/workspaceContentService.ts
@@ -3,7 +3,6 @@ import * as path from 'path';
 import * as fs from 'fs';
 
 import { WorkspaceContent } from ".";
-import { KIOTA_WORKSPACE_FILE } from "../../constants";
 import { getWorkspaceJsonPath } from '../../util';
 
 class WorkspaceContentService {
@@ -15,11 +14,7 @@ class WorkspaceContentService {
       return;
     }
     try {
-      const workspaceJson = vscode.workspace.textDocuments.find(doc => doc.fileName.endsWith(KIOTA_WORKSPACE_FILE));
-      if (!workspaceJson) {
-        throw new Error('Workspace file not found');
-      }
-      const content = workspaceJson.getText();
+      const content = await fs.promises.readFile(getWorkspaceJsonPath(), 'utf-8');
       return JSON.parse(content);
     } catch (error) {
       console.error('Error loading workspace.json:', error);
diff --git a/vscode/microsoft-kiota/src/providers/workspaceTreeProvider.ts b/vscode/microsoft-kiota/src/providers/workspaceTreeProvider.ts
index a7ed3f9943..51563edfdf 100644
--- a/vscode/microsoft-kiota/src/providers/workspaceTreeProvider.ts
+++ b/vscode/microsoft-kiota/src/providers/workspaceTreeProvider.ts
@@ -29,7 +29,6 @@ export class WorkspaceTreeProvider implements vscode.TreeDataProvider<WorkspaceT
     private workspaceContentService: WorkspaceContentService,
     private sharedService: SharedService
   ) {
-    void this.loadContent();
   }
 
   async refreshView(): Promise<void> {
@@ -47,11 +46,15 @@ export class WorkspaceTreeProvider implements vscode.TreeDataProvider<WorkspaceT
     }
 
     if (!element) {
-      const hasClients = this.workspaceContent?.clients && Object.keys(this.workspaceContent.clients).length > 0;
-      const hasPlugins = this.workspaceContent?.plugins && Object.keys(this.workspaceContent.plugins).length > 0;
-      const collapsibleState = (hasClients || hasPlugins) ? vscode.TreeItemCollapsibleState.Expanded : vscode.TreeItemCollapsibleState.Collapsed;
-      return [
-        new WorkspaceTreeItem(KIOTA_WORKSPACE_FILE, collapsibleState, 'root')
+      const hasClients = this.workspaceContent.clients && Object.keys(this.workspaceContent.clients).length > 0;
+      const hasPlugins = this.workspaceContent.plugins && Object.keys(this.workspaceContent.plugins).length > 0;
+      const hasWorkspaceContent = hasClients || hasPlugins;
+      return hasWorkspaceContent ? [
+        new WorkspaceTreeItem(KIOTA_WORKSPACE_FILE,
+          vscode.TreeItemCollapsibleState.Expanded, 'root')
+      ] : [
+        new WorkspaceTreeItem(vscode.l10n.t('No clients or plugins are available'),
+          vscode.TreeItemCollapsibleState.None, 'info')
       ];
     }
 
@@ -64,9 +67,6 @@ export class WorkspaceTreeProvider implements vscode.TreeDataProvider<WorkspaceT
         if (Object.keys(this.workspaceContent.plugins).length > 0) {
           children.push(new WorkspaceTreeItem(PLUGINS, vscode.TreeItemCollapsibleState.Expanded, 'category'));
         }
-        if (children.length === 0) {
-          children.push(new WorkspaceTreeItem(vscode.l10n.t("No clients or plugins are available"), vscode.TreeItemCollapsibleState.None, 'info'));
-        }
         return children;
       }
 
@@ -101,7 +101,7 @@ export class WorkspaceTreeProvider implements vscode.TreeDataProvider<WorkspaceT
       case 'root':
         element.command = {
           command: 'kiota.workspace.openWorkspaceFile',
-          title: vscode.l10n.t("Open File"),
+          title: vscode.l10n.t('Open File'),
           arguments: [vscode.Uri.file(getWorkspaceJsonPath())]
         };
         element.contextValue = 'folder';
@@ -139,12 +139,5 @@ export async function loadTreeView(context: vscode.ExtensionContext, treeDataPro
       await vscode.commands.executeCommand('kiota.editPaths', label, properties, category);
     })
   );
-  context.subscriptions.push(
-    vscode.workspace.onDidChangeTextDocument(async (event: vscode.TextDocumentChangeEvent) => {
-      const document = event.document;
-      if (document.fileName.endsWith(KIOTA_WORKSPACE_FILE)) {
-        await vscode.commands.executeCommand('kiota.workspace.refresh');
-      }
-    })
-  );
+  await vscode.commands.executeCommand('kiota.workspace.refresh');
 };
\ No newline at end of file


Test patch:
diff --git a/vscode/microsoft-kiota/src/test/suite/commands/deleteWorkspaceItemCommand.test.ts b/vscode/microsoft-kiota/src/test/suite/commands/deleteWorkspaceItemCommand.test.ts
index b431e29e36..9b6debfb39 100644
--- a/vscode/microsoft-kiota/src/test/suite/commands/deleteWorkspaceItemCommand.test.ts
+++ b/vscode/microsoft-kiota/src/test/suite/commands/deleteWorkspaceItemCommand.test.ts
@@ -3,6 +3,8 @@ import * as sinon from "sinon";
 import * as vscode from 'vscode';
 
 import { DeleteWorkspaceItemCommand } from '../../../commands/deleteWorkspaceItem/deleteWorkspaceItemCommand';
+import * as treeModule from "../../../providers/openApiTreeProvider";
+import * as sharedServiceModule from '../../../providers/sharedService';
 import { WorkspaceTreeItem } from '../../../providers/workspaceTreeProvider';
 
 suite('DeleteWorkspaceItemCommand Tests', () => {
@@ -14,7 +16,9 @@ suite('DeleteWorkspaceItemCommand Tests', () => {
   setup(() => {
     context = { extension: { packageJSON: { telemetryInstrumentationKey: 'test-key' } } } as any;
     outputChannel = { appendLine: sinon.stub() } as any;
-    command = new DeleteWorkspaceItemCommand(context, outputChannel);
+    var treeProvider = sinon.createStubInstance(treeModule.OpenApiTreeProvider);
+    var stubbedSharedService = sinon.createStubInstance(sharedServiceModule.SharedService);
+    command = new DeleteWorkspaceItemCommand(context, treeProvider, outputChannel, stubbedSharedService,);
     workspaceTreeItem = { label: 'test-item', category: 'plugin' } as any;
   });