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..0bb2da2bf3 --- /dev/null +++ b/src/providers/moduleProvider.ts @@ -0,0 +1,161 @@ +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); + if (this.version != '') { + this.tooltip = `${this.provider}@${this.version}`; + this.description = `${this.provider}@${this.version}`; + } else { + this.tooltip = `${this.provider}`; + this.description = `${this.provider}`; + } + } + + 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; + }); + } +}