@@ -6,6 +6,47 @@ import { execSync } from "node:child_process";
66interface 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
1152function 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
129136export 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