Skip to content

Commit

Permalink
Terraform Module View Provider
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jpogran committed Sep 23, 2021
1 parent f656f1c commit e914098
Show file tree
Hide file tree
Showing 4 changed files with 215 additions and 0 deletions.
4 changes: 4 additions & 0 deletions assets/icons/terraform.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
52 changes: 52 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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'"
}
]
},
Expand Down
6 changes: 6 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -135,6 +136,11 @@ export async function activate(context: vscode.ExtensionContext): Promise<any> {
}
}

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 };
}
Expand Down
153 changes: 153 additions & 0 deletions src/providers/moduleProvider.ts
Original file line number Diff line number Diff line change
@@ -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<ModuleCall> {
private _onDidChangeTreeData: vscode.EventEmitter<ModuleCall | undefined | null | void> = new vscode.EventEmitter<
ModuleCall | undefined | null | void
>();
readonly onDidChangeTreeData: vscode.Event<ModuleCall | undefined | null | void> = 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<ModuleCall> {
return element;
}

getChildren(element?: ModuleCall): vscode.ProviderResult<ModuleCall[]> {
if (element) {
return Promise.resolve(element.children);
} else {
const m = this.getModules();
return Promise.resolve(m);
}
}

async getModules(): Promise<ModuleCall[]> {
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);
}
}

0 comments on commit e914098

Please sign in to comment.