Skip to content

Commit a3fc78f

Browse files
authored
Merge pull request #670 from CodinGame/lmn/sandbox-mode
[FEAT] Support sandbox mode: allow reinitializing the workbench
2 parents b98e27f + 9e8915d commit a3fc78f

19 files changed

+5806
-83
lines changed

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,32 @@ Add this plugin in your configuration:
397397
}
398398
```
399399

400+
## Sandbox mode (⚠️ beta ⚠️)
401+
402+
One issue with VSCode is it's only designed to be initialized once. So the initialization options (workbench options, remote server authority...) can't be updated/reloaded. Also it's not possible to "unload" the services.
403+
404+
To still be able to do it, a possibility is to run all VSCode code inside an iframe instead of in the main page. But then VSCode will render in the iframe only and won't be well integrated in the page.
405+
406+
To better integrate it, it's also possible to run the code in the iframe, but make the code interact with the main page dom.
407+
408+
This library supports that mode. To enable that, you should
409+
- have a secondary html entrypoint, that initialize the services
410+
- load that secondary html in an iframe
411+
- in the iframe, set `window.vscodeWindow` to the parent window, also initialize the service with a container mounted in that window
412+
- do not import any monaco-vscode-library from the top window, but you can declare functions on the iframe window to get objects to the top window
413+
414+
To "unload" the workbench, you should:
415+
- remove the iframe element from the top frame
416+
- remove or empty the workbench container
417+
- cleanup the elements that VSCode has injected in the page head: `document.querySelectorAll('[data-vscode]').forEach((el) => el.remove())`
418+
419+
⚠️ `window.vscodeWindow` should be set BEFORE any VSCode code is loaded
420+
421+
422+
Note: it can be used in combination with shadow dom
423+
424+
It's demonstrated in the demo, by adding `?sandbox` query parameter to the demo url
425+
400426
## Troubleshooting
401427

402428
If something doesn't work, make sure to check out the [Troubleshooting](https://github.com/CodinGame/monaco-vscode-api/wiki/Troubleshooting) wiki page.

demo/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
66
<title>monaco-vscode-api demo</title>
77
<link rel="icon" href="/favicon.ico" type="image/x-icon">
8-
<script type="module" src="/src/loader.ts"></script>
8+
<script type="module" src="/src/entry.ts"></script>
99
</head>
1010
<body>
1111
</body>

demo/package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

demo/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,22 @@
1818
},
1919
"devDependencies": {
2020
"@codingame/esbuild-import-meta-url-plugin": "^1.0.3",
21-
"@types/dockerode": "^3.3.41",
21+
"@types/dockerode": "^3.3.42",
2222
"@types/express": "^5.0.3",
2323
"@types/node": "^20.11.4",
2424
"@types/wicg-file-system-access": "^2023.10.6",
2525
"@types/ws": "^8.18.1",
2626
"patch-package": "^8.0.0",
2727
"typescript": "~5.8.3",
28-
"vite": "~7.0.0",
28+
"vite": "~7.0.6",
2929
"@codingame/monaco-vscode-rollup-extension-directory-plugin": "file:../dist/packages/monaco-vscode-rollup-extension-directory-plugin",
3030
"@codingame/monaco-vscode-rollup-vsix-plugin": "file:../dist/packages/monaco-vscode-rollup-vsix-plugin"
3131
},
3232
"dependencies": {
3333
"ansi-colors": "^4.1.3",
3434
"dockerode": "^4.0.7",
3535
"express": "^5.1.0",
36-
"ws": "^8.18.2",
36+
"ws": "^8.18.3",
3737
"@codingame/monaco-vscode-07eaa805-9dea-5ec6-a422-a4f04872424d-common": "file:../dist/packages/monaco-vscode-07eaa805-9dea-5ec6-a422-a4f04872424d-common",
3838
"@codingame/monaco-vscode-0b087f42-a5a3-5eb9-9bfd-1eebc1bba163-common": "file:../dist/packages/monaco-vscode-0b087f42-a5a3-5eb9-9bfd-1eebc1bba163-common",
3939
"@codingame/monaco-vscode-0c06bfba-d24d-5c4d-90cd-b40cefb7f811-common": "file:../dist/packages/monaco-vscode-0c06bfba-d24d-5c4d-90cd-b40cefb7f811-common",

demo/src/entry.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const searchParams = new URLSearchParams(window.location.search)
2+
3+
const sandbox = searchParams.has('sandbox')
4+
if (sandbox) {
5+
;(async () => {
6+
await import('./sandbox')
7+
})()
8+
} else {
9+
;(async () => {
10+
await import('./loader')
11+
})()
12+
}

demo/src/loader.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,26 @@ if (locale != null) {
5656
}
5757

5858
const mode = searchParams.get('mode')
59+
const sandboxed = searchParams.has('sandboxed')
5960

60-
if (mode === 'full-workbench') {
61-
void import('./main.workbench')
62-
} else {
63-
void import('./main.views')
61+
;(async () => {
62+
if (sandboxed) {
63+
window.vscodeContainer = await new Promise<HTMLElement>((resolve) => {
64+
window.start = resolve
65+
window.parent.postMessage('WAITING')
66+
})
67+
window.vscodeWindow = window.vscodeContainer.ownerDocument.defaultView!
68+
}
69+
if (mode === 'full-workbench') {
70+
await import('./main.workbench')
71+
} else {
72+
await import('./main.views')
73+
}
74+
})()
75+
76+
declare global {
77+
var vscodeContainer: HTMLElement
78+
var start: (container: HTMLElement) => void
6479
}
6580

6681
export {}

demo/src/sandbox.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import './style.css'
2+
3+
const searchParams = new URLSearchParams()
4+
searchParams.set('mode', 'full-workbench')
5+
searchParams.set('sandboxed', '')
6+
7+
const container = document.createElement('div')
8+
container.style.height = '100vh'
9+
container.style.display = 'flex'
10+
container.style.flexDirection = 'column'
11+
12+
document.body.appendChild(container)
13+
14+
function load(): Disposable {
15+
const wrapper = document.createElement('div')
16+
wrapper.style.flex = '1'
17+
wrapper.style.display = 'flex'
18+
container.append(wrapper)
19+
const shadowRoot = wrapper.attachShadow({
20+
mode: 'open'
21+
})
22+
23+
const workbenchElement = document.createElement('div')
24+
workbenchElement.style.position = 'relative'
25+
workbenchElement.style.flex = '1'
26+
workbenchElement.style.maxWidth = '100%'
27+
shadowRoot.appendChild(workbenchElement)
28+
29+
const loader = document.createElement('div')
30+
loader.style.position = 'absolute'
31+
loader.style.left = '0'
32+
loader.style.right = '0'
33+
loader.style.bottom = '0'
34+
loader.style.top = '0'
35+
loader.style.display = 'flex'
36+
loader.style.alignItems = 'center'
37+
loader.style.justifyContent = 'center'
38+
loader.style.border = '1px solid red'
39+
loader.textContent = 'Loading...'
40+
workbenchElement.appendChild(loader)
41+
42+
const iframe = document.createElement('iframe')
43+
iframe.src = window.location.origin + '?' + searchParams?.toString()
44+
iframe.loading = 'eager'
45+
iframe.style.display = 'none'
46+
document.body.appendChild(iframe)
47+
48+
window.addEventListener('message', (event) => {
49+
if (event.data === 'WAITING' && event.source === iframe.contentWindow) {
50+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
51+
;(iframe.contentWindow as any)?.start(workbenchElement)
52+
}
53+
})
54+
55+
return {
56+
[Symbol.dispose]() {
57+
iframe.remove()
58+
wrapper.remove()
59+
document.querySelectorAll('[data-vscode]').forEach((el) => el.remove())
60+
}
61+
}
62+
}
63+
64+
let disposable = load()
65+
66+
function reload() {
67+
console.log('reloading...')
68+
disposable[Symbol.dispose]()
69+
disposable = load()
70+
}
71+
72+
const buttons = document.createElement('div')
73+
74+
const serverUrlInput = document.createElement('input')
75+
serverUrlInput.style.width = '350px'
76+
serverUrlInput.type = 'text'
77+
serverUrlInput.placeholder = 'remoteAuthority/remotePath?'
78+
serverUrlInput.addEventListener('change', () => {
79+
searchParams.delete('remotePath')
80+
searchParams.delete('remoteAuthority')
81+
if (serverUrlInput.value.trim().length > 0) {
82+
const url = new URL('ws://' + serverUrlInput.value)
83+
searchParams.append('remoteAuthority', url.host)
84+
if (url.pathname.length > 0) {
85+
searchParams.append('remotePath', url.pathname)
86+
}
87+
}
88+
reload()
89+
})
90+
buttons.appendChild(serverUrlInput)
91+
92+
const reinitializeButton = document.createElement('button')
93+
reinitializeButton.textContent = 'Reinitialize the workbench'
94+
reinitializeButton.addEventListener('click', reload)
95+
buttons.appendChild(reinitializeButton)
96+
97+
container.prepend(buttons)
98+
99+
const header = document.createElement('h1')
100+
header.textContent = 'Sandbox mode: reinitialize the workbench without reloading the page'
101+
container.prepend(header)

demo/src/setup.workbench.ts

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,24 @@ import {
2121
disableShadowDom
2222
} from './setup.common'
2323

24-
let container = document.createElement('div')
25-
container.style.height = '100vh'
24+
let container = window.vscodeContainer
2625

27-
document.body.replaceChildren(container)
26+
if (container == null) {
27+
container = document.createElement('div')
28+
container.style.height = '100vh'
2829

29-
if (!disableShadowDom) {
30-
const shadowRoot = container.attachShadow({
31-
mode: 'open'
32-
})
30+
document.body.replaceChildren(container)
3331

34-
const workbenchElement = document.createElement('div')
35-
workbenchElement.style.height = '100vh'
36-
shadowRoot.appendChild(workbenchElement)
37-
container = workbenchElement
32+
if (!disableShadowDom) {
33+
const shadowRoot = container.attachShadow({
34+
mode: 'open'
35+
})
36+
37+
const workbenchElement = document.createElement('div')
38+
workbenchElement.style.height = '100vh'
39+
shadowRoot.appendChild(workbenchElement)
40+
container = workbenchElement
41+
}
3842
}
3943

4044
const buttons = document.createElement('div')
@@ -48,7 +52,7 @@ buttons.innerHTML = `
4852
<br />
4953
<button id="togglePanel">Toggle Panel</button>
5054
<button id="toggleAuxiliary">Toggle Secondary Panel</button>
51-
</div>
55+
<button id="toggleSandbox">Switch to sandbox rendering mode</button>
5256
`
5357
document.body.append(buttons)
5458

@@ -79,6 +83,13 @@ document.querySelector('#toggleAuxiliary')!.addEventListener('click', async () =
7983
)
8084
})
8185

86+
document.querySelector('#toggleSandbox')!.addEventListener('click', async () => {
87+
const url = new URL(window.location.href)
88+
url.search = ''
89+
url.searchParams.append('sandbox', '')
90+
window.location.href = url.toString()
91+
})
92+
8293
export async function clearStorage(): Promise<void> {
8394
await userDataProvider.reset()
8495
await ((await getService(IStorageService)) as BrowserStorageService).clear()

docs/vscode_monaco_upgrade.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
- Shadow dom mode or not
3636
- Using VSCode server
3737
- Using HTML file system provider
38+
- Sandbox mode
3839
- ...
3940
- Make the commit that updates the vscode version a breaking change commit: by adding `!` before the `:` in the commit message
4041

package-lock.json

Lines changed: 12 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)