From 938d2a98a460f916196290d01eabad5fe8d97f1c Mon Sep 17 00:00:00 2001 From: James Pogran Date: Mon, 13 Sep 2021 13:27:09 -0400 Subject: [PATCH] Terraform Module View Provider This adds a new view to the Explorer view container in the activity bar that shows a list of modules referenced in the module opened. Each module is styled based on its type, and will show dependent modules that can be expanded. It also introduces two new commands, `refresh` and `open documentation`, that are tied to the view and can be used in the view title bar as well as in context menus for each module. This view will be only active when the extension activates, and will only display information when a module is open that references modules. If no modules are found, a note is left in the window. This also introduces a new approach to organizing the code in feature files instead of entirely inside the extension.ts file. --- assets/icons/terraform.svg | 4 + package.json | 42 +++++++++ src/extension.ts | 6 ++ src/providers/moduleProvider.ts | 156 ++++++++++++++++++++++++++++++++ 4 files changed, 208 insertions(+) create mode 100644 assets/icons/terraform.svg create mode 100644 src/providers/moduleProvider.ts diff --git a/assets/icons/terraform.svg b/assets/icons/terraform.svg new file mode 100644 index 0000000000..82636954ab --- /dev/null +++ b/assets/icons/terraform.svg @@ -0,0 +1,4 @@ + + + + diff --git a/package.json b/package.json index f8470a27f7..05ef835c70 100644 --- a/package.json +++ b/package.json @@ -211,6 +211,48 @@ { "command": "terraform.plan", "title": "Terraform: plan" + }, + { + "command": "terraform.modules.refreshList", + "title": "Refresh", + "icon": "$(refresh)" + }, + { + "command": "terraform.modules.documentation", + "title": "Open Documentation", + "icon": "$(book)" + } + ], + "menus": { + "view/title": [ + { + "command": "terraform.modules.refreshList", + "when": "view == terraform.modules", + "group": "navigation" + } + ], + "view/item/context": [ + { + "command": "terraform.modules.documentation", + "when": "view == terraform.modules" + } + ] + }, + "views": { + "explorer": [ + { + "id": "terraform.modules", + "name": "Terraform Module Calls", + "icon": "assets/icons/terraform.svg", + "visibility": "collapsed", + "when": "terraform.showModuleView" + } + ] + }, + "viewsWelcome": [ + { + "view": "terraform.modules", + "contents": "The active editor cannot provide module information. [Learn more](https://www.terraform.io/docs/language/modules/develop/index.html)" } ] }, diff --git a/src/extension.ts b/src/extension.ts index 39a0ca58df..0efa4e0836 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -5,6 +5,7 @@ import { LanguageClient } from 'vscode-languageclient/node'; import { Utils } from 'vscode-uri'; import { ClientHandler, TerraformLanguageClient } from './clientHandler'; import { defaultVersionString, isValidVersionString, LanguageServerInstaller } from './languageServerInstaller'; +import { TerraformModuleProvider } from './providers/moduleProvider'; import { ServerPath } from './serverPath'; import { SingleInstanceTimeout } from './utils'; import { config, prunedFolderNames } from './vscodeUtils'; @@ -161,6 +162,11 @@ export async function activate(context: vscode.ExtensionContext): Promise { } } + vscode.commands.executeCommand('setContext', 'terraform.showModuleView', true); + context.subscriptions.push( + vscode.window.registerTreeDataProvider('terraform.modules', new TerraformModuleProvider(context, clientHandler)), + ); + // export public API return { clientHandler, moduleCallers }; } diff --git a/src/providers/moduleProvider.ts b/src/providers/moduleProvider.ts new file mode 100644 index 0000000000..1ff0765e71 --- /dev/null +++ b/src/providers/moduleProvider.ts @@ -0,0 +1,156 @@ +import * as path from 'path'; +import * as vscode from 'vscode'; +import { ExecuteCommandParams, ExecuteCommandRequest } from 'vscode-languageclient'; +import { Utils } from 'vscode-uri'; +import { ClientHandler } from '../clientHandler'; + +const LOCALMODULE = new vscode.ThemeIcon('symbol-folder', new vscode.ThemeColor('terminal.ansiBrightBlue')); +const TFREGISTRY = new vscode.ThemeIcon('extensions-view-icon', new vscode.ThemeColor('terminal.ansiBrightMagenta')); +const GITHUBMODULE = new vscode.ThemeIcon('github'); + +class TerraformModule extends vscode.TreeItem { + constructor( + public label: string, + public provider: string, + public version: string, + public type: string, + public doclink: string, + public state: vscode.TreeItemCollapsibleState, + public readonly children?: [TerraformModule], + ) { + super(label, state); + this.tooltip = `${this.provider}@${this.version}`; + this.description = `${this.provider}@${this.version}`; + } + + iconPath = this.getIcon(this.type); + + getIcon(type: string) { + const icon = this.terraformIcon(); + switch (type) { + case 'tfregistry': + return icon; + case 'local': + return LOCALMODULE; + case 'github': + return GITHUBMODULE; + default: + return TFREGISTRY; + } + } + + private terraformIcon() { + // need current extension path to find icon svg + // could possibly make this a custom icon + const myExtDir = vscode.extensions.getExtension('hashicorp.terraform').extensionPath; + const svg = vscode.Uri.file(path.join(myExtDir, 'assets', 'icons', 'terraform.svg')); + const icon = { + light: svg, + dark: svg, + }; + return icon; + } +} + +export class TerraformModuleProvider implements vscode.TreeDataProvider { + private _onDidChangeTreeData: vscode.EventEmitter = + new vscode.EventEmitter(); + readonly onDidChangeTreeData: vscode.Event = + this._onDidChangeTreeData.event; + + constructor(ctx: vscode.ExtensionContext, public handler: ClientHandler) { + ctx.subscriptions.push( + vscode.commands.registerCommand('terraform.modules.refreshList', () => this.refresh()), + vscode.commands.registerCommand('terraform.modules.documentation', (module: TerraformModule) => { + vscode.env.openExternal(vscode.Uri.parse(module.doclink)); + }), + vscode.window.onDidChangeActiveTextEditor(async (event: vscode.TextEditor | undefined) => { + if (event && vscode.workspace.workspaceFolders[0]) { + this.refresh(); + } + }), + ); + } + + refresh(): void { + this._onDidChangeTreeData.fire(); + } + + getTreeItem(element: TerraformModule): TerraformModule | Thenable { + return element; + } + + getChildren(element?: TerraformModule): vscode.ProviderResult { + if (element) { + return Promise.resolve(element.children); + } else { + const m = this.getModules(); + return Promise.resolve(m); + } + } + + getCollapseState(type: string): vscode.TreeItemCollapsibleState { + switch (type) { + case 'tfregistry': + return vscode.TreeItemCollapsibleState.Collapsed; + case 'local': + return vscode.TreeItemCollapsibleState.None; + case 'github': + return vscode.TreeItemCollapsibleState.None; + default: + return vscode.TreeItemCollapsibleState.None; + } + } + + async getModules(): Promise { + const activeEditor = vscode.window.activeTextEditor; + if (activeEditor === undefined) { + return Promise.resolve([]); + } + + const document = activeEditor.document; + if (document === undefined) { + return Promise.resolve([]); + } + + const editor = document.uri; + const documentURI = Utils.dirname(editor); + const handler = this.handler.getClient(editor); + + return await handler.client.onReady().then(async () => { + const params: ExecuteCommandParams = { + command: `${handler.commandPrefix}.terraform-ls.terraform.modulelist`, + arguments: [`uri=${documentURI}`], + }; + + const response = await handler.client.sendRequest(ExecuteCommandRequest.type, params); + if (response == null) { + return Promise.resolve([]); + } + + const list = response.modules.map((m) => { + let deps: [TerraformModule]; + if (m.depmodules === null) { + deps = null; + } else { + deps = m.depmodules.map((dp) => { + return new TerraformModule( + dp.name, + dp.path, + dp.version, + dp.type, + dp.docklink, + vscode.TreeItemCollapsibleState.None, + ); + }); + } + + const state = this.getCollapseState(m.type); + + return new TerraformModule(m.name, m.path, m.version, m.type, m.docklink, state, deps); + }); + + return list; + }); + } +}