Skip to content

Commit 1247c80

Browse files
committed
(GH-666) Puppetfile view
Adds a new view to the Puppet toolbar view container that shows the puppet modules listed in the Puppetfile in the current workspace. This also adds a new command and context menu that will open the Puppetfile at the line of the selected module in the view. This uses the `openTextDocument` VS Code API to open the Puppetfile without showing the file to the user. This initiates a `didOpen` notification which sends the content to the Language Server. If we opened the document and sent the content ourselves, a `didOpen` notitication is still sent, so it would result in sending the content twice.
1 parent 8e6c8b7 commit 1247c80

File tree

4 files changed

+192
-1
lines changed

4 files changed

+192
-1
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how
66

77
## [Unreleased]
88

9+
### Added
10+
11+
- ([GH-666](https://github.com/puppetlabs/puppet-vscode/issues/666)) Add Puppetfile view to Puppet ToolBar
12+
913
### Changed
1014

1115
- ([GH-649](https://github.com/puppetlabs/puppet-vscode/issues/649)) Reduce activation events for extension

package.json

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"version": "0.26.1",
66
"editorComponents": {
77
"editorServices": {
8-
"release": "0.26.0"
8+
"release": "0.26.1"
99
},
1010
"editorSyntax": {
1111
"release": "1.3.6"
@@ -194,6 +194,24 @@
194194
"light": "assets/icons/light/sync.svg",
195195
"dark": "assets/icons/dark/sync.svg"
196196
}
197+
},
198+
{
199+
"command": "puppet.goToPuppetfileDefinition",
200+
"title": "Go to definition",
201+
"enablement": "puppet:puppetfileEnabled",
202+
"icon": {
203+
"light": "assets/icons/light/sync.svg",
204+
"dark": "assets/icons/dark/sync.svg"
205+
}
206+
},
207+
{
208+
"command": "puppet.refreshPuppetfileDependencies",
209+
"title": "Refresh Puppetfile View",
210+
"enablement": "puppet:puppetfileEnabled",
211+
"icon": {
212+
"light": "assets/icons/light/sync.svg",
213+
"dark": "assets/icons/dark/sync.svg"
214+
}
197215
}
198216
],
199217
"viewsContainers": {
@@ -209,13 +227,23 @@
209227
{
210228
"view": "puppetFacts",
211229
"contents": "No facts found\n[Refresh](command:puppet.refreshFacts)"
230+
},
231+
{
232+
"view": "puppetfile",
233+
"contents": "No Puppetfile found\n[Refresh](command:puppet.refreshPuppetfileDependencies)",
234+
"when": "puppet:puppetfileEnabled"
212235
}
213236
],
214237
"views": {
215238
"puppet-toolbar": [
216239
{
217240
"id": "puppetFacts",
218241
"name": "Facts"
242+
},
243+
{
244+
"id": "puppetfile",
245+
"name": "Puppetfile",
246+
"when": "puppet:puppetfileEnabled"
219247
}
220248
]
221249
},
@@ -323,6 +351,12 @@
323351
"when": "view == puppetFacts",
324352
"group": "navigation"
325353
}
354+
],
355+
"view/item/context": [
356+
{
357+
"command": "puppet.goToPuppetfileDefinition",
358+
"when": "view == puppetfile"
359+
}
326360
]
327361
},
328362
"configurationDefaults": {

src/extension.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
'use strict';
33

44
import * as fs from 'fs';
5+
import * as path from 'path';
56
import * as vscode from 'vscode';
67
import { CreateAggregrateConfiguration, IAggregateConfiguration } from './configuration';
78
import { IFeature } from './feature';
@@ -22,6 +23,7 @@ import { OutputChannelLogger } from './logging/outputchannel';
2223
import { ConnectionType, legacySettings, ProtocolType, PuppetInstallType, SettingsFromWorkspace } from './settings';
2324
import { reporter } from './telemetry';
2425
import { PuppetFactsProvider } from './views/facts';
26+
import { PuppetfileProvider } from './views/puppetfile';
2527

2628
// eslint-disable-next-line @typescript-eslint/no-var-requires
2729
const axios = require('axios');
@@ -68,6 +70,12 @@ export function activate(context: vscode.ExtensionContext) {
6870
pdkVersion: configSettings.ruby.pdkVersion,
6971
});
7072

73+
const puppetfile = path.join(vscode.workspace.workspaceFolders[0].uri.fsPath, 'Puppetfile');
74+
const exists = fs.existsSync(puppetfile);
75+
if (exists && configSettings.workspace.editorService.enable) {
76+
vscode.commands.executeCommand('setContext', 'puppet:puppetfileEnabled', true);
77+
}
78+
7179
const statusBar = new PuppetStatusBarFeature([puppetLangID, puppetFileLangID], configSettings, logger, context);
7280

7381
extensionFeatures = [
@@ -120,6 +128,9 @@ export function activate(context: vscode.ExtensionContext) {
120128

121129
const facts = new PuppetFactsProvider(connectionHandler);
122130
vscode.window.registerTreeDataProvider('puppetFacts', facts);
131+
132+
const puppetfileView = new PuppetfileProvider(connectionHandler);
133+
vscode.window.registerTreeDataProvider('puppetfile', puppetfileView);
123134
}
124135

125136
export function deactivate() {

src/views/puppetfile.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import * as path from 'path';
2+
import {
3+
commands,
4+
Event,
5+
EventEmitter,
6+
ProviderResult,
7+
ThemeIcon,
8+
TreeDataProvider,
9+
TreeItem,
10+
TreeItemCollapsibleState,
11+
Uri,
12+
ViewColumn,
13+
window,
14+
workspace,
15+
} from 'vscode';
16+
import { RequestType } from 'vscode-languageclient';
17+
import { ConnectionHandler } from '../handler';
18+
import { reporter } from '../telemetry';
19+
20+
class PuppetfileDependencyItem extends TreeItem {
21+
constructor(
22+
public readonly name: string,
23+
public readonly version: string,
24+
public readonly start_line: number,
25+
public readonly end_line: number,
26+
public readonly collapsibleState: TreeItemCollapsibleState,
27+
public readonly children?: Array<[string, PuppetfileDependencyItem]>,
28+
) {
29+
super(name, collapsibleState);
30+
if (children) {
31+
this.iconPath = ThemeIcon.Folder;
32+
} else {
33+
this.iconPath = new ThemeIcon('package');
34+
}
35+
}
36+
37+
get tooltip(): string {
38+
return `${this.name}-${this.version}`;
39+
}
40+
41+
get description(): string {
42+
return this.version;
43+
}
44+
}
45+
46+
class PuppetfileDependency {
47+
constructor(
48+
public readonly name: string,
49+
public readonly version: string,
50+
public readonly start_line: number,
51+
public readonly end_line: number,
52+
) {
53+
//
54+
}
55+
}
56+
57+
interface PuppetfileDependencyResponse {
58+
dependencies: PuppetfileDependency[];
59+
error: string[];
60+
}
61+
62+
export class PuppetfileProvider implements TreeDataProvider<PuppetfileDependencyItem> {
63+
private _onDidChangeTreeData: EventEmitter<PuppetfileDependencyItem | undefined> = new EventEmitter<
64+
PuppetfileDependencyItem | undefined
65+
>();
66+
readonly onDidChangeTreeData: Event<PuppetfileDependencyItem | undefined>;
67+
68+
constructor(protected handler: ConnectionHandler) {
69+
this.onDidChangeTreeData = this._onDidChangeTreeData.event;
70+
commands.registerCommand('puppet.refreshPuppetfileDependencies', () => {
71+
reporter.sendTelemetryEvent('puppet.refreshPuppetfileDependencies');
72+
this.refresh();
73+
});
74+
commands.registerCommand('puppet.goToPuppetfileDefinition', (puppetModule: PuppetfileDependencyItem) => {
75+
reporter.sendTelemetryEvent('puppet.goToPuppetfileDefinition');
76+
77+
const workspaceFolder = workspace.workspaceFolders[0].uri;
78+
const puppetfile = path.join(workspaceFolder.fsPath, 'Puppetfile');
79+
workspace.openTextDocument(puppetfile).then((doc) => {
80+
const line = doc.lineAt(+puppetModule.start_line);
81+
window.showTextDocument(doc, {
82+
preserveFocus: true,
83+
preview: false,
84+
selection: line.range,
85+
viewColumn: ViewColumn.Active,
86+
});
87+
});
88+
});
89+
}
90+
91+
refresh(): void {
92+
this._onDidChangeTreeData.fire(null);
93+
}
94+
95+
getTreeItem(element: PuppetfileDependencyItem): TreeItem | Thenable<PuppetfileDependencyItem> {
96+
return element;
97+
}
98+
99+
getChildren(element?: PuppetfileDependencyItem): Promise<PuppetfileDependencyItem[]> {
100+
if (element) {
101+
return Promise.resolve(element.children.map((e) => e[1]));
102+
} else {
103+
return this.getPuppetfileDependenciesFromLanguageServer();
104+
}
105+
}
106+
107+
private async getPuppetfileDependenciesFromLanguageServer(): Promise<PuppetfileDependencyItem[]> {
108+
await this.handler.languageClient.onReady();
109+
110+
const fileUri = Uri.file(path.join(workspace.workspaceFolders[0].uri.fsPath, 'Puppetfile'));
111+
/*
112+
We use openTextDocument here because we need to parse whether or not a user has opened a
113+
Puppetfile or not. This triggers onDidOpen notification which sends the content of the Puppetfile
114+
to the Puppet Language Server which caches the content for puppetfile-resolver to parse
115+
*/
116+
return workspace.openTextDocument(fileUri).then(async () => {
117+
const results = await this.handler.languageClient.sendRequest(
118+
new RequestType<never, PuppetfileDependencyResponse, void, void>('puppetfile/getDependencies'),
119+
{
120+
uri: fileUri.toString(),
121+
},
122+
);
123+
124+
reporter.sendTelemetryEvent('puppetfileView');
125+
126+
if (results.error) {
127+
window.showErrorMessage(`${results.error}`);
128+
}
129+
130+
const list = results.dependencies.map((d) => {
131+
return new PuppetfileDependencyItem(d.name, d.version, d.start_line, d.end_line, TreeItemCollapsibleState.None);
132+
});
133+
134+
return list;
135+
});
136+
}
137+
138+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
139+
getParent?(element: PuppetfileDependencyItem): ProviderResult<PuppetfileDependencyItem> {
140+
throw new Error('Method not implemented.');
141+
}
142+
}

0 commit comments

Comments
 (0)