Skip to content

Commit 4ec15cf

Browse files
committed
vibe
1 parent 65a5867 commit 4ec15cf

File tree

2 files changed

+152
-72
lines changed

2 files changed

+152
-72
lines changed

vsextension/package.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@
2727
{
2828
"command": "projspec.openProject",
2929
"title": "Open Project"
30+
},
31+
{
32+
"command": "projspec.showJson",
33+
"title": "Show"
3034
}
3135
],
3236
"views": {
@@ -41,7 +45,12 @@
4145
"view/item/context": [
4246
{
4347
"command": "projspec.openProject",
44-
"when": "view == projspec-projects",
48+
"when": "view == projspec-projects && viewItem == project",
49+
"group": "navigation"
50+
},
51+
{
52+
"command": "projspec.showJson",
53+
"when": "view == projspec-projects && viewItem == project",
4554
"group": "navigation"
4655
}
4756
]

vsextension/src/extension.ts

Lines changed: 142 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,47 @@ import { execSync } from "node:child_process";
66
interface TreeNode {
77
key: string;
88
children?: TreeNode[];
9+
data?: any;
10+
tooltip?: string;
11+
isProject?: boolean;
12+
projectUrl?: string;
13+
}
14+
15+
const openDocuments = new Map<string, vscode.TextDocument>();
16+
17+
function buildTreeNodes(projectUrl: string, project: any): TreeNode[] {
18+
const projectChildren: TreeNode[] = [];
19+
20+
// Top-level contents
21+
if (project.contents && Object.keys(project.contents).length > 0) {
22+
const contentChildren: TreeNode[] = [];
23+
for (const [name, _] of Object.entries(project.contents)) {
24+
const basename = name.split('/').pop() || name;
25+
contentChildren.push({ key: basename, tooltip: name, projectUrl });
26+
}
27+
projectChildren.push({ key: "contents", children: contentChildren });
28+
}
29+
30+
// Top-level artifacts
31+
if (project.artifacts && Object.keys(project.artifacts).length > 0) {
32+
const artifactChildren: TreeNode[] = [];
33+
for (const [name, _] of Object.entries(project.artifacts)) {
34+
const basename = name.split('/').pop() || name;
35+
artifactChildren.push({ key: basename, tooltip: name, projectUrl });
36+
}
37+
projectChildren.push({ key: "artifacts", children: artifactChildren });
38+
}
39+
40+
// Specs - only show spec names, not their details
41+
if (project.specs && Object.keys(project.specs).length > 0) {
42+
const specsChildren: TreeNode[] = [];
43+
for (const [specName, _] of Object.entries(project.specs as Record<string, any>)) {
44+
specsChildren.push({ key: specName, projectUrl });
45+
}
46+
projectChildren.push({ key: "specs", children: specsChildren });
47+
}
48+
49+
return projectChildren;
950
}
1051

1152
function getExampleData(): TreeNode {
@@ -17,56 +58,11 @@ function getExampleData(): TreeNode {
1758

1859
// Data is a dict of project_url -> project_data
1960
for (const [projectUrl, project] of Object.entries(data)) {
20-
const projectChildren: TreeNode[] = [];
61+
const projectChildren = buildTreeNodes(projectUrl, project);
2162

22-
// Top-level contents
23-
if (project.contents && Object.keys(project.contents).length > 0) {
24-
const contentChildren: TreeNode[] = [];
25-
for (const [name, _] of Object.entries(project.contents)) {
26-
contentChildren.push({ key: name });
27-
}
28-
projectChildren.push({ key: "contents", children: contentChildren });
29-
}
30-
31-
// Top-level artifacts
32-
if (project.artifacts && Object.keys(project.artifacts).length > 0) {
33-
const artifactChildren: TreeNode[] = [];
34-
for (const [name, _] of Object.entries(project.artifacts)) {
35-
artifactChildren.push({ key: name });
36-
}
37-
projectChildren.push({ key: "artifacts", children: artifactChildren });
38-
}
39-
40-
// Specs
41-
if (project.specs && Object.keys(project.specs).length > 0) {
42-
const specsChildren: TreeNode[] = [];
43-
for (const [specName, spec] of Object.entries(project.specs as Record<string, any>)) {
44-
const specChildren: TreeNode[] = [];
45-
46-
// Spec contents
47-
if (spec._contents && Object.keys(spec._contents).length > 0) {
48-
const specContentChildren: TreeNode[] = [];
49-
for (const [name, _] of Object.entries(spec._contents as Record<string, any>)) {
50-
specContentChildren.push({ key: name });
51-
}
52-
specChildren.push({ key: "contents", children: specContentChildren });
53-
}
54-
55-
// Spec artifacts
56-
if (spec._artifacts && Object.keys(spec._artifacts).length > 0) {
57-
const specArtifactChildren: TreeNode[] = [];
58-
for (const [name, _] of Object.entries(spec._artifacts as Record<string, any>)) {
59-
specArtifactChildren.push({ key: name });
60-
}
61-
specChildren.push({ key: "artifacts", children: specArtifactChildren });
62-
}
63-
64-
specsChildren.push({ key: specName, children: specChildren });
65-
}
66-
projectChildren.push({ key: "specs", children: specsChildren });
67-
}
68-
69-
children.push({ key: projectUrl, children: projectChildren });
63+
// Extract basename from project URL for display
64+
const projectBasename = projectUrl.split('/').pop() || projectUrl;
65+
children.push({ key: projectBasename, tooltip: projectUrl, children: projectChildren, data: project, isProject: true });
7066
}
7167

7268
return { key: "projects", children };
@@ -75,20 +71,17 @@ function getExampleData(): TreeNode {
7571
key: "projects",
7672
children: [
7773
{
78-
key: "https://example.com/project-alpha",
74+
key: "project-alpha",
75+
tooltip: "https://example.com/project-alpha",
76+
projectUrl: "https://example.com/project-alpha",
7977
children: [
80-
{ key: "contents", children: [{ key: "README.md" }, { key: "setup.py" }] },
81-
{ key: "artifacts", children: [{ key: "dist/pkg.tar" }] },
78+
{ key: "contents", children: [{ key: "README.md", tooltip: "README.md", projectUrl: "https://example.com/project-alpha" }, { key: "setup.py", tooltip: "setup.py", projectUrl: "https://example.com/project-alpha" }] },
79+
{ key: "artifacts", children: [{ key: "dist/pkg.tar", tooltip: "dist/pkg.tar", projectUrl: "https://example.com/project-alpha" }] },
8280
{
8381
key: "specs",
8482
children: [
85-
{
86-
key: "v1.0.0",
87-
children: [
88-
{ key: "contents", children: [{ key: "spec.md" }] },
89-
{ key: "artifacts", children: [{ key: "output.html" }] }
90-
]
91-
}
83+
{ key: "v1.0.0", projectUrl: "https://example.com/project-alpha" },
84+
{ key: "v2.0.0", projectUrl: "https://example.com/project-alpha" }
9285
]
9386
}
9487
]
@@ -115,6 +108,20 @@ class ProjectTreeDataProvider implements vscode.TreeDataProvider<TreeNode> {
115108
} else {
116109
item.collapsibleState = vscode.TreeItemCollapsibleState.None;
117110
}
111+
if (element.tooltip) {
112+
item.tooltip = element.tooltip;
113+
}
114+
if (element.isProject) {
115+
item.contextValue = 'project';
116+
}
117+
// Set command for leaf nodes that belong to a project
118+
if (!element.isProject && element.projectUrl && !element.children) {
119+
item.command = {
120+
command: 'projspec.showItem',
121+
arguments: [element],
122+
title: 'Show Item'
123+
};
124+
}
118125
return item;
119126
}
120127

@@ -128,7 +135,7 @@ class ProjectTreeDataProvider implements vscode.TreeDataProvider<TreeNode> {
128135

129136
export function activate(context: vscode.ExtensionContext) {
130137

131-
// register a content provider for scheme
138+
// register a content provider for scheme
132139
const myScheme = 'projspec';
133140
const myProvider = new class implements vscode.TextDocumentContentProvider {
134141

@@ -137,25 +144,19 @@ export function activate(context: vscode.ExtensionContext) {
137144
onDidChange = this.onDidChangeEmitter.event;
138145

139146
provideTextDocumentContent(uri: vscode.Uri): string {
140-
const out = execSync("projspec --html-out " + uri.toString().substring(18), { stdio: 'pipe' });
147+
const out = execSync("projspec --html-out " + uri.toString().substring(18), { stdio: 'pipe' });
141148
return uri.toString().substring(18) + out;
142149
}
143150
};
144151

145-
context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider(myScheme, myProvider));
152+
context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider(myScheme, myProvider));
146153

147154
context.subscriptions.push(vscode.commands.registerCommand('projspec.scan', async () => {
148-
// const what = await vscode.window.showInputBox({ placeHolder: 'where...' });
149155
if (vscode.workspace.workspaceFolders !== undefined) {
150156
const folderPath = vscode.workspace.workspaceFolders[0].uri.fsPath;
151157
const uri = vscode.Uri.file(folderPath);
152-
// vscode.commands.executeCommand('vscode.openFolder', uri, { forceReuseWindow: true });
153158
let text = vscode.Uri.parse("projspec:" + uri);
154159

155-
// text format
156-
// const doc = await vscode.workspace.openTextDocument(text);
157-
//await vscode.window.showTextDocument(doc, { preview: false });
158-
159160
const panel = vscode.window.createWebviewPanel("projspec", folderPath, vscode.ViewColumn.One, {});
160161
panel.webview.html = "<!DOCTYPE html><html><body>" + to_html(text.toString()) + "</body></html>";
161162
console.log(folderPath);
@@ -172,11 +173,11 @@ export function activate(context: vscode.ExtensionContext) {
172173
}));
173174

174175
context.subscriptions.push(vscode.commands.registerCommand('projspec.openProject', async (item: TreeNode) => {
175-
if (!item || !item.key) {
176+
if (!item || !item.tooltip) {
176177
return;
177178
}
178179

179-
const projectUrl = item.key;
180+
const projectUrl = item.tooltip;
180181

181182
// Only handle file:// URLs
182183
if (projectUrl.startsWith('file://')) {
@@ -189,7 +190,77 @@ export function activate(context: vscode.ExtensionContext) {
189190
vscode.window.showErrorMessage(`Unsupported project URL scheme: ${projectUrl}`);
190191
}
191192
}));
192-
// /Users/mdurant/code/projspec
193+
194+
context.subscriptions.push(vscode.commands.registerCommand('projspec.showJson', async (item: TreeNode) => {
195+
if (!item || !item.key) {
196+
return;
197+
}
198+
199+
const jsonContent = JSON.stringify(item.data || {}, null, 2);
200+
const doc = await vscode.workspace.openTextDocument({
201+
language: 'json',
202+
content: jsonContent
203+
});
204+
await vscode.window.showTextDocument(doc, { preview: false });
205+
}));
206+
207+
context.subscriptions.push(vscode.commands.registerCommand('projspec.showItem', async (item: TreeNode) => {
208+
if (!item || !item.projectUrl) {
209+
return;
210+
}
211+
212+
try {
213+
const out = execSync("projspec library list --json-out", { stdio: 'pipe', encoding: 'utf-8' });
214+
const data = JSON.parse(out) as Record<string, any>;
215+
const projectData = data[item.projectUrl];
216+
217+
if (!projectData) {
218+
vscode.window.showErrorMessage(`Project not found: ${item.projectUrl}`);
219+
return;
220+
}
221+
222+
const jsonContent = JSON.stringify(projectData, null, 2);
223+
224+
// Check if document is already open
225+
const existingDoc = openDocuments.get(item.projectUrl);
226+
let doc: vscode.TextDocument;
227+
228+
if (existingDoc) {
229+
doc = existingDoc;
230+
} else {
231+
doc = await vscode.workspace.openTextDocument({
232+
language: 'json',
233+
content: jsonContent
234+
});
235+
openDocuments.set(item.projectUrl, doc);
236+
}
237+
238+
const editor = await vscode.window.showTextDocument(doc, {
239+
preview: false,
240+
viewColumn: vscode.ViewColumn.One
241+
});
242+
243+
// Try to find and reveal the item in the JSON
244+
const text = doc.getText();
245+
const searchKey = item.tooltip || item.key;
246+
247+
if (searchKey) {
248+
const searchText = searchKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
249+
const regex = new RegExp(`"${searchText}"`);
250+
const match = text.match(regex);
251+
252+
if (match) {
253+
const start = doc.positionAt(match.index!);
254+
const end = doc.positionAt(match.index! + match[0].length);
255+
const range = new vscode.Range(start, end);
256+
editor.selection = new vscode.Selection(start, end);
257+
editor.revealRange(range, vscode.TextEditorRevealType.InCenter);
258+
}
259+
}
260+
} catch (error) {
261+
vscode.window.showErrorMessage(`Failed to load project: ${error}`);
262+
}
263+
}));
193264

194265
console.log('Congratulations, your extension "projspec" is now active!');
195266
}

0 commit comments

Comments
 (0)