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 @@ + + NovĂ˝ projekt + + + + + \ 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); + } + } +} +