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 67945aab2e..d825a20146 100644 --- a/package.json +++ b/package.json @@ -213,6 +213,58 @@ { "command": "terraform.plan", "title": "Terraform: plan" + }, + { + "command": "terraform.modules.refreshList", + "title": "Refresh", + "icon": "$(refresh)" + }, + { + "command": "terraform.modules.openDocumentation", + "title": "Open Documentation", + "icon": "$(book)" + } + ], + "menus": { + "commandPalette": [ + { + "command": "terraform.modules.refreshList", + "when": "false" + }, + { + "command": "terraform.modules.openDocumentation", + "when": "false" + } + ], + "view/title": [ + { + "command": "terraform.modules.refreshList", + "when": "view == terraform.modules", + "group": "navigation" + } + ], + "view/item/context": [ + { + "command": "terraform.modules.openDocumentation", + "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 information about installed modules. [Learn more about modules](https://www.terraform.io/docs/language/modules/develop/index.html) You may need to run 'terraform get'" } ] }, diff --git a/src/extension.ts b/src/extension.ts index 782fc27e6c..62d4c0870b 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 { ModuleProvider } from './providers/moduleProvider'; import { ServerPath } from './serverPath'; import { SingleInstanceTimeout } from './utils'; import { config, getActiveTextEditor, prunedFolderNames } from './vscodeUtils'; @@ -135,6 +136,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 ModuleProvider(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..6c184a985d --- /dev/null +++ b/src/providers/moduleProvider.ts @@ -0,0 +1,153 @@ +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'; +import { getActiveTextEditor } from '../vscodeUtils'; + +class ModuleCall extends vscode.TreeItem { + constructor( + public label: string, + public sourceAddr: string, + public version: string, + public sourceType: string, + public docsLink: string, + public terraformIcon: string, + public readonly children: ModuleCall[], + ) { + super( + label, + children.length >= 1 ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.None, + ); + + this.description = this.version ? `${this.version}` : ''; + + if (this.version === undefined) { + this.tooltip = `${this.sourceAddr}`; + } else { + this.tooltip = `${this.sourceAddr}@${this.version}`; + } + } + + iconPath = this.getIcon(this.sourceType); + + getIcon(type: string) { + switch (type) { + case 'tfregistry': + return { + light: this.terraformIcon, + dark: this.terraformIcon, + }; + case 'local': + return new vscode.ThemeIcon('symbol-folder'); + case 'github': + return new vscode.ThemeIcon('github'); + case 'git': + return new vscode.ThemeIcon('git-branch'); + default: + return new vscode.ThemeIcon('extensions-view-icon'); + } + } +} + +export class ModuleProvider implements vscode.TreeDataProvider { + private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter< + ModuleCall | undefined | null | void + >(); + readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; + + private svg = ''; + + constructor(ctx: vscode.ExtensionContext, public handler: ClientHandler) { + this.svg = ctx.asAbsolutePath(path.join('assets', 'icons', 'terraform.svg')); + ctx.subscriptions.push( + vscode.commands.registerCommand('terraform.modules.refreshList', () => this.refresh()), + vscode.commands.registerCommand('terraform.modules.openDocumentation', (module: ModuleCall) => { + vscode.env.openExternal(vscode.Uri.parse(module.docsLink)); + }), + vscode.window.onDidChangeActiveTextEditor(async (event: vscode.TextEditor | undefined) => { + if (event && getActiveTextEditor()) { + this.refresh(); + } + }), + ); + } + + refresh(): void { + this._onDidChangeTreeData.fire(); + } + + getTreeItem(element: ModuleCall): ModuleCall | Thenable { + return element; + } + + getChildren(element?: ModuleCall): vscode.ProviderResult { + if (element) { + return Promise.resolve(element.children); + } else { + const m = this.getModules(); + return Promise.resolve(m); + } + } + + async getModules(): Promise { + const activeEditor = getActiveTextEditor(); + 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.module.calls`, + arguments: [`uri=${documentURI}`], + }; + + const response = await handler.client.sendRequest(ExecuteCommandRequest.type, params); + if (response == null) { + return Promise.resolve([]); + } + + const list = response.module_calls.map((m) => + this.toModuleCall(m.name, m.source_addr, m.version, m.source_type, m.docs_link, this.svg, m.dependent_modules), + ); + + return list; + }); + } + + toModuleCall( + name: string, + sourceAddr: string, + version: string, + sourceType: string, + docsLink: string, + terraformIcon: string, + dependents: any, + ): ModuleCall { + let deps: ModuleCall[] = []; + if (dependents.length != 0) { + deps = dependents.map((dp) => + this.toModuleCall( + dp.name, + dp.source_addr, + dp.version, + dp.source_type, + dp.docs_link, + terraformIcon, + dp.dependent_modules, + ), + ); + } + + return new ModuleCall(name, sourceAddr, version, sourceType, docsLink, terraformIcon, deps); + } +}