diff --git a/images/gsc-sidebar.svg b/images/gsc-sidebar.svg
new file mode 100644
index 0000000..f498de4
--- /dev/null
+++ b/images/gsc-sidebar.svg
@@ -0,0 +1,9 @@
+
\ No newline at end of file
diff --git a/package.json b/package.json
index 29b2c57..1800c03 100644
--- a/package.json
+++ b/package.json
@@ -45,6 +45,32 @@
"path": "./syntaxes/gsc.tmLanguage.json"
}
],
+ "viewsContainers": {
+ "activitybar": [
+ {
+ "id": "gsc-sidebar",
+ "title": "GSC",
+ "icon": "images/gsc-sidebar.svg"
+ }
+ ]
+ },
+ "views": {
+ "gsc-sidebar": [
+ {
+ "id": "gsc-view-workspace-info",
+ "name": "Workspace info"
+ },
+ {
+ "id": "gsc-view-file-info",
+ "name": "File info"
+ },
+ {
+ "id": "gsc-view-other",
+ "name": "Other",
+ "type": "webview"
+ }
+ ]
+ },
"commands": [
{
"command": "gsc.selectGame",
diff --git a/src/Events.ts b/src/Events.ts
index fce4d32..370195f 100644
--- a/src/Events.ts
+++ b/src/Events.ts
@@ -2,6 +2,7 @@ import * as vscode from 'vscode';
import { GscConfig } from './GscConfig';
import { GscFiles } from './GscFiles';
import { GscStatusBar } from './GscStatusBar';
+import { GscSidePanel } from './GscSidePanel';
import { GscFile } from './GscFile';
import { LoggerOutput } from './LoggerOutput';
import { Issues } from './Issues';
@@ -35,6 +36,7 @@ export class Events {
await GscFiles.onChangeWorkspaceFolders(e);
+ GscSidePanel.workspaceInfoProvider.refreshAll();
} catch (error) {
Issues.handleError(error);
}
@@ -61,6 +63,8 @@ export class Events {
LoggerOutput.log("[Events] Debounce done (250ms) - Active editor changed to " + e?.document.fileName);
await GscStatusBar.updateStatusBar("activeEditorChanged");
+
+ GscSidePanel.fileInfoProvider.refresh();
}, 250);
} catch (error) {
@@ -88,7 +92,10 @@ export class Events {
try {
//LoggerOutput.log("[Events] Editor selection changed.");
- GscFiles.onChangeEditorSelection(e);
+ if (e.kind !== undefined) {
+ GscFiles.onChangeEditorSelection(e);
+ }
+
} catch (error) {
Issues.handleError(error);
}
@@ -152,6 +159,11 @@ export class Events {
LoggerOutput.log("[Events] GSC file parsed", vscode.workspace.asRelativePath(gscFile.uri));
this.onDidGscFileParsedEvent.fire(gscFile);
+
+ // Refresh the side panel if the active editor is the parsed file
+ if (gscFile.uri.toString() === vscode.window.activeTextEditor?.document.uri.toString()) {
+ GscSidePanel.fileInfoProvider.refresh();
+ }
}
@@ -172,5 +184,6 @@ export class Events {
static GscFileCacheFileHasChanged(fileUri: vscode.Uri) {
LoggerOutput.log("[Events] GSC cache changed for file", vscode.workspace.asRelativePath(fileUri));
+ GscSidePanel.workspaceInfoProvider.refreshCachedGscFiles(fileUri);
}
}
\ No newline at end of file
diff --git a/src/Gsc.ts b/src/Gsc.ts
index 3ad6f3b..ebd97e0 100644
--- a/src/Gsc.ts
+++ b/src/Gsc.ts
@@ -9,6 +9,7 @@ import { GscStatusBar } from './GscStatusBar';
import { GscConfig } from './GscConfig';
import { GscCodeActionProvider } from './GscCodeActionProvider';
import { Issues } from './Issues';
+import { GscSidePanel } from './GscSidePanel';
import { LoggerOutput } from './LoggerOutput';
import { Events } from './Events';
@@ -22,6 +23,7 @@ export class Gsc {
// Register events
try {
+ await GscSidePanel.activate(context);
await GscConfig.activate(context);
await GscDiagnosticsCollection.activate(context);
await GscFiles.activate(context);
diff --git a/src/GscConfig.ts b/src/GscConfig.ts
index a16a2d9..3063ba1 100644
--- a/src/GscConfig.ts
+++ b/src/GscConfig.ts
@@ -3,7 +3,7 @@ import { LoggerOutput } from './LoggerOutput';
import { GscDiagnosticsCollection } from './GscDiagnosticsCollection';
import { GscFiles } from './GscFiles';
import { GscStatusBar } from './GscStatusBar';
-import { Issues } from './Issues';
+import { GscSidePanel } from './GscSidePanel';
// These must match with package.json settings
export enum GscGame {
@@ -125,6 +125,7 @@ export class GscConfig {
GscFiles.updateConfigurationOfCachedFiles();
// 2. Update tree view
+ GscSidePanel.workspaceInfoProvider.refreshIncludedWorkspaceFolders();
// 3. Update status bar in case the game has changed
await GscStatusBar.updateStatusBar("configChanged");
diff --git a/src/GscSidePanel.ts b/src/GscSidePanel.ts
new file mode 100644
index 0000000..8f8b68d
--- /dev/null
+++ b/src/GscSidePanel.ts
@@ -0,0 +1,26 @@
+import * as vscode from 'vscode';
+import { LoggerOutput } from './LoggerOutput';
+import { GscWorkspaceTreeDataProvider } from './GscSidePanelWorkspaceTreeDataProvider';
+import { GscFileTreeDataProvider } from './GscSidePanelFileTreeDataProvider';
+import { OtherViewProvider } from './GscSidePanelOtherViewProvider';
+
+export class GscSidePanel {
+ public static fileInfoProvider: GscFileTreeDataProvider;
+ public static workspaceInfoProvider: GscWorkspaceTreeDataProvider;
+ public static otherViewProvider: OtherViewProvider;
+
+ // Activates the logger and registers the necessary disposal function
+ static async activate(context: vscode.ExtensionContext) {
+ LoggerOutput.log("[GscSidePanel] Activating");
+
+ this.fileInfoProvider = new GscFileTreeDataProvider();
+ context.subscriptions.push(vscode.window.registerTreeDataProvider("gsc-view-file-info", GscSidePanel.fileInfoProvider));
+
+ this.workspaceInfoProvider = new GscWorkspaceTreeDataProvider();
+ context.subscriptions.push(vscode.window.registerTreeDataProvider("gsc-view-workspace-info", GscSidePanel.workspaceInfoProvider));
+
+ this.otherViewProvider = new OtherViewProvider();
+ context.subscriptions.push(vscode.window.registerWebviewViewProvider("gsc-view-other", GscSidePanel.otherViewProvider));
+ }
+
+}
\ No newline at end of file
diff --git a/src/GscSidePanelFileTreeDataProvider.ts b/src/GscSidePanelFileTreeDataProvider.ts
new file mode 100644
index 0000000..81c87d5
--- /dev/null
+++ b/src/GscSidePanelFileTreeDataProvider.ts
@@ -0,0 +1,88 @@
+import * as vscode from 'vscode';
+import { GscFiles } from './GscFiles';
+
+
+interface GscTreeItemData {
+ label: string;
+ originalLabel?: string;
+ children?: GscTreeItemData[];
+ command?: vscode.Command;
+
+ icon?: vscode.ThemeIcon;
+}
+
+enum GscTreeItem {
+ ReferenceableGameRootFolders = 'Referenceable Game Root Folders',
+ IgnoredFilePaths = 'Ignored File Paths',
+ IgnoredFunctionNames = 'Ignored Function Names',
+ CurrentGame = 'Current Game',
+ ErrorDiagnostics = 'Error Diagnostics'
+}
+
+
+export class GscFileTreeDataProvider implements vscode.TreeDataProvider {
+ private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter();
+ readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event;
+
+ constructor() {
+ }
+
+ getTreeItem(element: GscTreeItemData): vscode.TreeItem {
+ const treeItem = new vscode.TreeItem(
+ element.label,
+ element.children ? this.getCollapsibleState(element.originalLabel || element.label) : vscode.TreeItemCollapsibleState.None
+ );
+
+ return treeItem;
+ }
+
+ getChildren(element?: GscTreeItemData): GscTreeItemData[] {
+
+ // Its a root element
+ if (!element) {
+
+ const editor = vscode.window.activeTextEditor;
+ const gscFile = editor ? GscFiles.getCachedFile(editor.document.uri) : undefined;
+
+ if (editor && gscFile) {
+
+ const referenceableGameRootFolders = gscFile.config.referenceableGameRootFolders.map((folder: any) => ({ label: vscode.workspace.asRelativePath(folder.uri, true) }));
+ const ignoredFilePaths = gscFile.config.ignoredFilePaths.map((path: string) => ({ label: path }));
+ const ignoredFunctionNames = gscFile.config.ignoredFunctionNames.map((name: string) => ({ label: name }));
+ const currentGame = [{ label: gscFile.config.currentGame }];
+ const errorDiagnostics = [{ label: gscFile.config.errorDiagnostics }];
+
+ return [
+ { label: `${GscTreeItem.ReferenceableGameRootFolders} (${referenceableGameRootFolders.length})`, originalLabel: GscTreeItem.ReferenceableGameRootFolders, children: referenceableGameRootFolders },
+ { label: `${GscTreeItem.IgnoredFilePaths} (${ignoredFilePaths.length})`, originalLabel: GscTreeItem.IgnoredFilePaths, children: ignoredFilePaths },
+ { label: `${GscTreeItem.IgnoredFunctionNames} (${ignoredFunctionNames.length})`, originalLabel: GscTreeItem.IgnoredFunctionNames, children: ignoredFunctionNames },
+ { label: `${GscTreeItem.CurrentGame} (${currentGame.length})`, originalLabel: GscTreeItem.CurrentGame, children: currentGame },
+ { label: `${GscTreeItem.ErrorDiagnostics} (${errorDiagnostics.length})`, originalLabel: GscTreeItem.ErrorDiagnostics, children: errorDiagnostics }
+ ];
+ }
+
+ return [
+ { label: 'No active editor' }
+ ];
+ }
+ return element.children || [];
+ }
+
+ refresh(): void {
+ this._onDidChangeTreeData.fire();
+ }
+
+ private getCollapsibleState(label: string): vscode.TreeItemCollapsibleState {
+ switch (label) {
+ case GscTreeItem.ReferenceableGameRootFolders:
+ case GscTreeItem.IgnoredFilePaths:
+ case GscTreeItem.IgnoredFunctionNames:
+ return vscode.TreeItemCollapsibleState.Expanded;
+ default:
+ return vscode.TreeItemCollapsibleState.Collapsed;
+ }
+ }
+}
+
+
+
diff --git a/src/GscSidePanelOtherViewProvider.ts b/src/GscSidePanelOtherViewProvider.ts
new file mode 100644
index 0000000..da4ecf3
--- /dev/null
+++ b/src/GscSidePanelOtherViewProvider.ts
@@ -0,0 +1,104 @@
+import * as vscode from 'vscode';
+import { GscFiles } from './GscFiles';
+import { GscDiagnosticsCollection } from './GscDiagnosticsCollection';
+import { Issues } from './Issues';
+import { Updates } from './Updates';
+
+
+export class OtherViewProvider implements vscode.WebviewViewProvider {
+
+ private _view?: vscode.WebviewView;
+
+ constructor() {
+ this.updateWebviewContent();
+ }
+
+ resolveWebviewView(webviewView: vscode.WebviewView) {
+ this._view = webviewView;
+ webviewView.webview.options = { enableScripts: true };
+ webviewView.webview.html = this.getHtmlContent();
+
+ webviewView.webview.onDidReceiveMessage(message => {
+ switch (message.command) {
+ case 'parseAllFiles':
+ void GscFiles.parseAllFiles();
+ break;
+ case 'reDiagnoseAllFiles':
+ void GscDiagnosticsCollection.updateDiagnosticsForAll("sidePanel");
+ break;
+ case 'reportIssue':
+ Issues.showIssueWindow(false);
+ break;
+ case 'showExtensionUpdates':
+ Updates.showUpdateWindow();
+ break;
+ }
+ });
+ }
+
+ // Update the webview content based on the current active editor
+ public updateWebviewContent() {
+ if (!this._view) {
+ return;
+ }
+ this._view.webview.html = this.getHtmlContent();
+
+ }
+
+ private getHtmlContent(): string {
+ return `
+
+
+
+
+
+ GSC Files:
+
+
+
+ Issues:
+
+
+ Updates:
+
+
+
+
+ `;
+ }
+
+}
diff --git a/src/GscSidePanelWorkspaceTreeDataProvider.ts b/src/GscSidePanelWorkspaceTreeDataProvider.ts
new file mode 100644
index 0000000..a89dacb
--- /dev/null
+++ b/src/GscSidePanelWorkspaceTreeDataProvider.ts
@@ -0,0 +1,219 @@
+import * as vscode from 'vscode';
+import { GscFiles } from './GscFiles';
+import { GscConfig } from './GscConfig';
+import { LoggerOutput } from './LoggerOutput';
+
+enum GscWorkspaceTreeItemType {
+ WorkspaceFolder,
+ IncludedWorkspaceFolders,
+ IncludedWorkspaceFolder,
+ CachedGscFiles,
+ CachedGscFile,
+ CachedGscFileData
+}
+
+class GscWorkspaceTreeItem extends vscode.TreeItem {
+
+ constructor(
+ public readonly label: string,
+ public readonly type: GscWorkspaceTreeItemType,
+ public readonly workspace: vscode.WorkspaceFolder,
+ public readonly collapsibleState: vscode.TreeItemCollapsibleState,
+ public readonly icon?: vscode.ThemeIcon,
+
+ ) {
+ super(label, collapsibleState);
+ this.iconPath = icon;
+ }
+
+ children?: GscWorkspaceTreeItem[];
+
+ updateTimer?: NodeJS.Timeout;
+}
+
+
+export class GscWorkspaceTreeDataProvider implements vscode.TreeDataProvider {
+ private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter();
+ readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event;
+
+ private _cachedGscFiles: GscWorkspaceTreeItem[] = [];
+ private _includedWorkspaceFolders: GscWorkspaceTreeItem[] = [];
+
+ constructor() {
+ }
+
+ // Called also when onDidChangeTreeData is fired
+ getTreeItem(element: GscWorkspaceTreeItem): vscode.TreeItem {
+
+ //console.log("getTreeItem", element);
+
+ let treeItem: vscode.TreeItem;
+
+
+ switch (element.type) {
+ case GscWorkspaceTreeItemType.WorkspaceFolder:
+ treeItem = element;
+ break;
+
+ case GscWorkspaceTreeItemType.IncludedWorkspaceFolders:
+ treeItem = element;
+
+ // Update the children of "Cached GSC Files" now to get the count in advance, and save it into children to reuse later
+ const includedFolders = GscConfig.getIncludedWorkspaceFolders(element.workspace.uri).map(name => new GscWorkspaceTreeItem(
+ name,
+ GscWorkspaceTreeItemType.IncludedWorkspaceFolder,
+ element.workspace,
+ vscode.TreeItemCollapsibleState.None
+ ));
+ element.children = includedFolders;
+
+ treeItem.description = (element.children ? element.children.length : 0).toString();
+ break;
+
+
+ case GscWorkspaceTreeItemType.IncludedWorkspaceFolder:
+ treeItem = element;
+ break;
+
+
+
+ case GscWorkspaceTreeItemType.CachedGscFiles:
+ treeItem = element;
+
+ // Update the children of "Cached GSC Files" now to get the count in advance, and save it into children to reuse later
+ const cachedFiles = GscFiles.getCachedFiles([element.workspace.uri]).map((file, i) => new GscWorkspaceTreeItem(
+ vscode.workspace.asRelativePath(file.uri, false),
+ GscWorkspaceTreeItemType.CachedGscFile,
+ element.workspace,
+ vscode.TreeItemCollapsibleState.None
+ ));
+ element.children = cachedFiles;
+
+ treeItem.description = (element.children ? element.children.length : 0).toString();
+ break;
+
+ case GscWorkspaceTreeItemType.CachedGscFile:
+ treeItem = element;
+ break;
+
+ case GscWorkspaceTreeItemType.CachedGscFileData:
+ treeItem = element;
+ break;
+
+ default:
+ throw new Error("Unknown tree item type");
+ break;
+ }
+
+ return treeItem;
+ }
+
+ // Called also when parent item is expanded
+ getChildren(element?: GscWorkspaceTreeItem): GscWorkspaceTreeItem[] {
+ const items: GscWorkspaceTreeItem[] = [];
+
+ //console.log("getChildren", element);
+
+ // Its a root element
+ if (!element) {
+
+ // Add workspace folders
+ const workspaceFolders = vscode.workspace.workspaceFolders || [];
+ for (const workspaceFolder of workspaceFolders) {
+ items.push(new GscWorkspaceTreeItem(
+ workspaceFolder.name,
+ GscWorkspaceTreeItemType.WorkspaceFolder,
+ workspaceFolder,
+ vscode.TreeItemCollapsibleState.Expanded,
+ new vscode.ThemeIcon('folder'))
+ );
+ }
+ this._includedWorkspaceFolders.length = 0;
+ this._cachedGscFiles.length = 0;
+ return items;
+ }
+ else {
+ switch (element.type) {
+ case GscWorkspaceTreeItemType.WorkspaceFolder:
+
+ const includedWorkspaceFoldersItem = new GscWorkspaceTreeItem(
+ "Included Workspace Folders",
+ GscWorkspaceTreeItemType.IncludedWorkspaceFolders,
+ element.workspace,
+ vscode.TreeItemCollapsibleState.Collapsed
+ );
+ this._includedWorkspaceFolders.push(includedWorkspaceFoldersItem);
+ items.push(includedWorkspaceFoldersItem);
+
+
+ const cachedGscFilesItem = new GscWorkspaceTreeItem(
+ "Cached GSC Files",
+ GscWorkspaceTreeItemType.CachedGscFiles,
+ element.workspace,
+ vscode.TreeItemCollapsibleState.Collapsed
+ );
+ this._cachedGscFiles.push(cachedGscFilesItem);
+ items.push(cachedGscFilesItem);
+
+ return items;
+
+ case GscWorkspaceTreeItemType.IncludedWorkspaceFolders:
+ // Included workspace folders are generated when "Included Workspace Folders" is refreshed (to get the count), so use that
+ return element.children || [];
+
+
+ case GscWorkspaceTreeItemType.CachedGscFiles:
+ // Cached files are generated when "Cached GSC Files" is refreshed (to get the count), so use that
+ return element.children || [];
+
+
+ case GscWorkspaceTreeItemType.CachedGscFile:
+ break;
+ }
+ }
+
+ return element.children || [];
+ }
+
+ refreshAll(): void {
+ LoggerOutput.log("[GscSidePanel] Workspace view - refreshing all");
+
+ this._onDidChangeTreeData.fire();
+ }
+
+ refreshCachedGscFiles(fileUri: vscode.Uri): void {
+ //LoggerOutput.log("[GscSidePanel] Workspace view - debouncing update of 'Cached GSC files'", vscode.workspace.asRelativePath(fileUri));
+
+ const workspaceUri = vscode.workspace.getWorkspaceFolder(fileUri)?.uri;
+ const cachedGscFilesItem = this._cachedGscFiles.find(item => item.workspace?.uri.toString() === workspaceUri?.toString());
+
+ if (cachedGscFilesItem) {
+ //console.log("debuncing item to update", cachedGscFilesItem.workspace?.name);
+
+ if (cachedGscFilesItem.updateTimer) {
+ clearTimeout(cachedGscFilesItem.updateTimer);
+ }
+ cachedGscFilesItem.updateTimer = setTimeout(() => {
+ LoggerOutput.log("[GscSidePanel] Workspace view - refreshing 'Cached GSC files' for " + cachedGscFilesItem.workspace?.name + " (debounced)", vscode.workspace.asRelativePath(fileUri));
+
+ this._onDidChangeTreeData.fire(cachedGscFilesItem);
+ }, 500);
+
+ } else {
+ // Not found, it means the workspace is not in the tree.
+ // It gets refreshed by another event.
+ }
+ }
+
+
+
+ refreshIncludedWorkspaceFolders(): void {
+ LoggerOutput.log("[GscSidePanel] Workspace view - update of 'Included workspace folders', count: " + this._includedWorkspaceFolders.length);
+
+ //this._includedWorkspaceFolders;
+ for (const item of this._includedWorkspaceFolders) {
+ this._onDidChangeTreeData.fire(item);
+ }
+ }
+}
+