@@ -2,173 +2,187 @@ import * as path from 'path'
22import * as CR from 'typings'
33import * as vscode from 'vscode'
44
5+ const getNonce = ( ) :string => {
6+ let text = ''
7+ const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
8+ for ( let i = 0 ; i < 32 ; i ++ ) {
9+ text += possible . charAt ( Math . floor ( Math . random ( ) * possible . length ) )
10+ }
11+ return text
12+ }
13+
14+
515/**
616 * Manages React webview panels
717 */
818class ReactWebView {
9- //@ts -ignore
10- public loaded :boolean
11- private panel :vscode . WebviewPanel
12- private extensionPath :string
13- private disposables :vscode . Disposable [ ] = [ ]
14- private onReceive :any // TODO: properly type
15-
16- public constructor ( extensionPath :string ) {
17- this . extensionPath = extensionPath
18-
19- // Create and show a new webview panel
20- this . panel = this . createWebviewPanel ( vscode . ViewColumn . Two )
21-
22- // Set the webview's initial html content
23- this . panel . webview . html = this . getHtmlForWebview ( )
24-
25- // Listen for when the panel is disposed
26- // This happens when the user closes the panel or when the panel is closed programatically
27- this . panel . onDidDispose ( this . dispose , this , this . disposables )
28-
29- // Handle messages from the webview
30- const onReceive = ( action :string | CR . Action ) => {
31- // await loading of webview in React before proceeding with loaded state
32- if ( action === 'WEBVIEW_LOADED' ) {
33- this . loaded = true
34- } else {
35- vscode . commands . executeCommand ( 'coderoad.receive_action' , action )
36- }
37- }
38- this . panel . webview . onDidReceiveMessage ( onReceive , null , this . disposables )
39-
40- // update panel on changes
41- const updateWindows = ( ) => {
42- vscode . commands . executeCommand ( 'vscode.setEditorLayout' , {
43- orientation :0 ,
44- groups :[ { groups :[ { } ] , size :0.6 } , { groups :[ { } ] , size :0.4 } ] ,
45- } )
46- }
47-
48- // prevents new panels from going ontop of coderoad panel
49- vscode . window . onDidChangeActiveTextEditor ( ( textEditor ) => {
50- console . log ( 'onDidChangeActiveTextEditor' )
51- console . log ( textEditor )
52- if ( ! textEditor || textEditor . viewColumn !== vscode . ViewColumn . Two ) {
53- updateWindows ( )
54- }
55- } )
56- // // prevents moving coderoad panel on top of left panel
57- vscode . window . onDidChangeVisibleTextEditors ( ( textEditor ) => {
58- console . log ( 'onDidChangeVisibleTextEditors' )
59- updateWindows ( )
60- } )
61-
62- // TODO: prevent window from moving to the left when no windows remain on rights
63- }
64-
65- public createOrShow ( column :number , callback ?:( ) => void ) :void {
66- // If we already have a panel, show it.
67- // Otherwise, create a new panel.
68- if ( this . panel && this . panel . webview ) {
69- this . panel . reveal ( column )
70- } else {
71- this . panel = this . createWebviewPanel ( column )
72- }
73- if ( callback ) {
74- // listen for when webview is loaded
75- // unfortunately there is no easy way of doing this
76- const webPanelListener = setInterval ( ( ) => {
77- if ( this . loaded ) {
78- // callback tells editor the webview has loaded
79- setTimeout ( callback )
80- clearInterval ( webPanelListener )
81- }
82- } , 200 )
83- }
84- }
85-
86- public async postMessage ( action :CR . Action ) :Promise < void > {
87- // Send a message to the webview webview.
88- // You can send any JSON serializable data.
89- const success = await this . panel . webview . postMessage ( action )
90- if ( ! success ) {
91- throw new Error ( `Message post failure:${ JSON . stringify ( action ) } ` )
92- }
93- }
94-
95- private async dispose ( ) :Promise < void > {
96- // Clean up our resources
97- this . loaded = false
98- this . panel . dispose ( )
99- Promise . all ( this . disposables . map ( ( x ) => x . dispose ( ) ) )
100- }
101-
102- private createWebviewPanel ( column :number ) :vscode . WebviewPanel {
103- const viewType = 'CodeRoad'
104- const title = 'CodeRoad'
105- const config = {
106- // Enable javascript in the webview
107- enableScripts :true ,
108- // And restric the webview to only loading content from our extension's `media` directory.
109- localResourceRoots :[ vscode . Uri . file ( path . join ( this . extensionPath , 'build' ) ) ] ,
110- // prevents destroying the window when it is in the background
111- retainContextWhenHidden :true ,
112- }
113- return vscode . window . createWebviewPanel ( viewType , title , column , config )
114- }
115-
116- private getNonce ( ) :string {
117- let text = ''
118- const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
119- for ( let i = 0 ; i < 32 ; i ++ ) {
120- text += possible . charAt ( Math . floor ( Math . random ( ) * possible . length ) )
121- }
122- return text
123- }
124-
125- private getHtmlForWebview ( ) :string {
126- // eslint-disable-next-line
127- const manifest = require ( path . join ( this . extensionPath , 'build' , 'asset-manifest.json' ) )
128- const mainScript = manifest . files [ 'main.js' ]
129- // grab first chunk
130- const chunk = Object . keys ( manifest . files ) . filter ( f => f . match ( / ^ s t a t i c \/ j s \/ .+ \. j s $ / ) ) [ 0 ]
131- const chunkScript = manifest . files [ chunk ]
132- const mainStyle = manifest . files [ 'main.css' ]
133-
134- const scriptPathOnDisk = vscode . Uri . file ( path . join ( this . extensionPath , 'build' , mainScript ) )
135- const scriptUri = scriptPathOnDisk . with ( { scheme :'vscode-resource' } )
136- const chunkPathOnDisk = vscode . Uri . file ( path . join ( this . extensionPath , 'build' , chunkScript ) )
137- const chunkUri = chunkPathOnDisk . with ( { scheme :'vscode-resource' } )
138- const stylePathOnDisk = vscode . Uri . file ( path . join ( this . extensionPath , 'build' , mainStyle ) )
139- const styleUri = stylePathOnDisk . with ( { scheme :'vscode-resource' } )
140-
141- // Use a nonce to whitelist which scripts can be run
142- const [ n1 , n2 , n3 ] = [ 1 , 2 , 3 ] . map ( this . getNonce )
143-
144- return `<!DOCTYPE html>
19+ //@ts -ignore
20+ public loaded :boolean
21+ private panel :vscode . WebviewPanel
22+ private extensionPath :string
23+ private disposables :vscode . Disposable [ ] = [ ]
24+ private onReceive :any // TODO: properly type
25+
26+ public constructor ( extensionPath :string ) {
27+ this . extensionPath = extensionPath
28+
29+ // Create and show a new webview panel
30+ this . panel = this . createWebviewPanel ( vscode . ViewColumn . Two )
31+
32+ // Set the webview's initial html content
33+ this . panel . webview . html = this . getHtmlForWebview ( )
34+
35+ // Listen for when the panel is disposed
36+ // This happens when the user closes the panel or when the panel is closed programatically
37+ this . panel . onDidDispose ( this . dispose , this , this . disposables )
38+
39+ // Handle messages from the webview
40+ const onReceive = ( action :string | CR . Action ) => {
41+ // await loading of webview in React before proceeding with loaded state
42+ if ( action === 'WEBVIEW_LOADED' ) {
43+ this . loaded = true
44+ } else {
45+ vscode . commands . executeCommand ( 'coderoad.receive_action' , action )
46+ }
47+ }
48+ this . panel . webview . onDidReceiveMessage ( onReceive , null , this . disposables )
49+
50+ // update panel on changes
51+ const updateWindows = ( ) => {
52+ vscode . commands . executeCommand ( 'vscode.setEditorLayout' , {
53+ orientation :0 ,
54+ groups :[ { groups :[ { } ] , size :0.6 } , { groups :[ { } ] , size :0.4 } ] ,
55+ } )
56+ }
57+
58+ // prevents new panels from going ontop of coderoad panel
59+ vscode . window . onDidChangeActiveTextEditor ( ( textEditor ) => {
60+ console . log ( 'onDidChangeActiveTextEditor' )
61+ console . log ( textEditor )
62+ if ( ! textEditor || textEditor . viewColumn !== vscode . ViewColumn . Two ) {
63+ updateWindows ( )
64+ }
65+ } )
66+ // // prevents moving coderoad panel on top of left panel
67+ vscode . window . onDidChangeVisibleTextEditors ( ( textEditor ) => {
68+ console . log ( 'onDidChangeVisibleTextEditors' )
69+ updateWindows ( )
70+ } )
71+
72+ // TODO: prevent window from moving to the left when no windows remain on rights
73+ }
74+
75+ public createOrShow ( column :number , callback ?:( ) => void ) :void {
76+ // If we already have a panel, show it.
77+ // Otherwise, create a new panel.
78+ if ( this . panel && this . panel . webview ) {
79+ this . panel . reveal ( column )
80+ } else {
81+ this . panel = this . createWebviewPanel ( column )
82+ }
83+ if ( callback ) {
84+ // listen for when webview is loaded
85+ // unfortunately there is no easy way of doing this
86+ const webPanelListener = setInterval ( ( ) => {
87+ if ( this . loaded ) {
88+ // callback tells editor the webview has loaded
89+ setTimeout ( callback )
90+ clearInterval ( webPanelListener )
91+ }
92+ } , 200 )
93+ }
94+ }
95+
96+ public async postMessage ( action :CR . Action ) :Promise < void > {
97+ // Send a message to the webview webview.
98+ // You can send any JSON serializable data.
99+ const success = await this . panel . webview . postMessage ( action )
100+ if ( ! success ) {
101+ throw new Error ( `Message post failure:${ JSON . stringify ( action ) } ` )
102+ }
103+ }
104+
105+ private async dispose ( ) :Promise < void > {
106+ // Clean up our resources
107+ this . loaded = false
108+ this . panel . dispose ( )
109+ Promise . all ( this . disposables . map ( ( x ) => x . dispose ( ) ) )
110+ }
111+
112+ private createWebviewPanel ( column :number ) :vscode . WebviewPanel {
113+ const viewType = 'CodeRoad'
114+ const title = 'CodeRoad'
115+ const config = {
116+ // Enable javascript in the webview
117+ enableScripts :true ,
118+ // And restric the webview to only loading content from our extension's `media` directory.
119+ localResourceRoots :[ vscode . Uri . file ( path . join ( this . extensionPath , 'build' ) ) ] ,
120+ // prevents destroying the window when it is in the background
121+ retainContextWhenHidden :true ,
122+ }
123+ return vscode . window . createWebviewPanel ( viewType , title , column , config )
124+ }
125+
126+ private getHtmlForWebview ( ) :string {
127+ const buildUri = vscode . Uri . file ( path . join ( this . extensionPath , 'build' ) ) . with ( { scheme :'vscode-resource' } )
128+
129+ const manifest = require ( path . join ( this . extensionPath , 'build' , 'asset-manifest.json' ) )
130+
131+ const mainStyle = manifest . files [ 'main.css' ]
132+ const stylePathOnDisk = vscode . Uri . file ( path . join ( this . extensionPath , 'build' , mainStyle ) )
133+ const styleUri = stylePathOnDisk . with ( { scheme :'vscode-resource' } )
134+
135+ const getSrc = ( manifestName :string ) :any => {
136+ const file = manifest . files [ manifestName ]
137+ const uriPath = vscode . Uri . file ( path . join ( this . extensionPath , 'build' , file ) )
138+ return uriPath . with ( { scheme :'vscode-resource' } )
139+ }
140+
141+ // map over scripts
142+ const scripts = [ {
143+ file :'./webpackBuild.js' ,
144+ } , {
145+ manifest :'runtime~main.js' ,
146+ } , {
147+ manifest :'main.js' ,
148+ } , {
149+ // grab first file that matches chunk
150+ manifest :Object . keys ( manifest . files ) . filter ( f => f . match ( / ^ s t a t i c \/ j s \/ .+ \. j s $ / ) ) [ 0 ] ,
151+ } ] . map ( script => ( {
152+ nonce :getNonce ( ) ,
153+ src :script . manifest ?getSrc ( script . manifest ) :script . file
154+ } ) )
155+
156+
157+ const indexHtml = `<!DOCTYPE html>
145158<html lang='en'>
146- <head>
147- <meta charset='utf-8'>
148- <meta name='viewport' content='width=device-width,initial-scale=1,shrink-to-fit=no'>
149- <meta name='theme-color' content='#000000'>
150- <title>React App</title>
151- <link rel='manifest' href='./manifest.json' />
152-
153- <!-- TODO: load styles through package -->
154- <link rel='stylesheet' href='https://unpkg.com/@alifd/next/dist/next.css' />
155- <link rel='stylesheet' type='text/css' href='${ styleUri } '>
156-
157- <meta http-equiv='Content-Security-Policy' content="font-src *; img-src vscode-resource: https:; script-src 'nonce-${ n1 } ' 'nonce-${ n2 } ' 'nonce-${ n3 } '; style-src vscode-resource: 'unsafe-inline' http: https: data:;">
158- <base href='${ vscode . Uri . file ( path . join ( this . extensionPath , 'build' ) ) . with ( {
159- scheme :'vscode-resource' ,
160- } ) } /'>
161- </head>
162-
163- <body>
164- <noscript>You need to enable JavaScript to run this app.</noscript>
165- <div id='root'>Loading...</div>
166- <script nonce=${ n1 } src='./webpackBuild.js'></script>
167- <script nonce=${ n2 } src='${ chunkUri } '></script>
168- <script nonce='${ n3 } ' src='${ scriptUri } '></script>
169- </body>
170- </html>`
171- }
159+ <head>
160+ <meta charset='utf-8'>
161+ <meta name='viewport' content='width=device-width,initial-scale=1,shrink-to-fit=no'>
162+ <meta name='theme-color' content='#000000'>
163+ <title>React App</title>
164+ <link rel='manifest' href='./manifest.json' />
165+
166+ <!-- TODO: load styles through package -->
167+ <!-- <link rel='stylesheet' href='https://unpkg.com/@alifd/next/dist/next.css' /> -->
168+ <!-- <link rel='stylesheet' type='text/css' href='${ styleUri } '> -->
169+
170+ <meta http-equiv='Content-Security-Policy' content="font-src *; img-src vscode-resource: https:; script-src${ scripts . map ( script => `'nonce-${ script . nonce } '` ) . join ( ' ' ) } ; style-src vscode-resource: 'unsafe-inline' http: https: data:;">
171+ <base href='${ buildUri } /'>
172+ </head>
173+
174+ <body>
175+ <noscript>You need to enable JavaScript to run this app.</noscript>
176+ <div id='root'>Loading...</div>
177+ ${ scripts . map ( s => `<script nonce='${ s . nonce } ' src='${ s . src } '></script>` ) . join ( '\n' ) }
178+ </body>
179+ </html>`
180+
181+ console . log ( indexHtml )
182+
183+ return indexHtml
184+ }
185+
172186}
173187
174188export default ReactWebView