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 14, 2021
1 parent 8b122d3 commit 5950a72
Show file tree
Hide file tree
Showing 4 changed files with 213 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.
42 changes: 42 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
}
]
},
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 { TerraformModuleProvider } from './providers/moduleProvider';
import { ServerPath } from './serverPath';
import { SingleInstanceTimeout } from './utils';
import { config, prunedFolderNames } from './vscodeUtils';
Expand Down Expand Up @@ -161,6 +162,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 TerraformModuleProvider(context, clientHandler)),
);

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

getChildren(element?: TerraformModule): vscode.ProviderResult<TerraformModule[]> {
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<TerraformModule[]> {
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;
});
}
}

0 comments on commit 5950a72

Please sign in to comment.