snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
This commit is contained in:
6
apps/vscode/extension/.gitignore
vendored
Normal file
6
apps/vscode/extension/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
dist
|
||||
editor
|
||||
node_modules
|
||||
.vscode-test-web/
|
||||
*.vsix
|
||||
.DS_Store
|
||||
5
apps/vscode/extension/.vscode/extensions.json
vendored
Normal file
5
apps/vscode/extension/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||
// for the documentation about the extensions.json format
|
||||
"recommendations": ["amodio.tsl-problem-matcher"]
|
||||
}
|
||||
23
apps/vscode/extension/.vscode/launch.json
vendored
Normal file
23
apps/vscode/extension/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
// A launch configuration that compiles the extension and then opens it inside a new window
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Run Web Extension ",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"args": [
|
||||
"--disable-extensions",
|
||||
"--extensionDevelopmentPath=${workspaceFolder}",
|
||||
"${workspaceFolder}/examples"
|
||||
],
|
||||
"outFiles": ["${workspaceFolder}/dist/web/**/*.js", "!**/node_modules/**"],
|
||||
"skipFiles": ["<node_internals>/**", "**/node_modules/**"],
|
||||
"sourceMaps": true
|
||||
}
|
||||
]
|
||||
}
|
||||
11
apps/vscode/extension/.vscode/settings.json
vendored
Normal file
11
apps/vscode/extension/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
// Place your settings in this file to overwrite default and user settings.
|
||||
{
|
||||
"files.exclude": {
|
||||
"out": false // set this to true to hide the "out" folder with the compiled JS files
|
||||
},
|
||||
"search.exclude": {
|
||||
"out": true // set this to false to include "out" folder in search results
|
||||
},
|
||||
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
|
||||
"typescript.tsc.autoDetect": "off"
|
||||
}
|
||||
0
apps/vscode/extension/.vscodeignore
Normal file
0
apps/vscode/extension/.vscodeignore
Normal file
1
apps/vscode/extension/CHANGELOG.md
Normal file
1
apps/vscode/extension/CHANGELOG.md
Normal file
@@ -0,0 +1 @@
|
||||
See [tldraw.dev](https://tldraw.dev/releases) for release notes.
|
||||
9
apps/vscode/extension/LICENSE.md
Normal file
9
apps/vscode/extension/LICENSE.md
Normal file
@@ -0,0 +1,9 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 tldraw Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
56
apps/vscode/extension/README.md
Normal file
56
apps/vscode/extension/README.md
Normal file
@@ -0,0 +1,56 @@
|
||||
## Introduction
|
||||
|
||||
[tldraw](https://tldraw.com) is a very good whiteboard for the web. This extension will let you use tldraw inside of VS Code, using offline `.tldr` files. Create drawings, diagrams, or visual documentation, and then commit the files alongside your code!
|
||||
|
||||

|
||||
|
||||
## Usage
|
||||
|
||||
- To create a new tldraw file, use the provided command: "tldraw: New tldraw File".
|
||||
|
||||

|
||||
|
||||
- To view an existing tldraw file, open a file with the `.tldr` extension in VS Code.
|
||||
|
||||
> **Tip:** The `.tldr` files you create with this extension can also be opened in the tldraw [web app](https://tldraw.com). You can also use this extension to open `.tldr` files saved from the web app.
|
||||
|
||||
## Releases
|
||||
|
||||
This extension is automatically released to the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=tldraw-org.tldraw-vscode) when changes are merged:
|
||||
|
||||
- **Pre-releases**: Merges to `main` branch trigger pre-release builds
|
||||
- **Production releases**: Merges to `production` branch trigger stable releases
|
||||
|
||||
### Direct Downloads
|
||||
|
||||
You can also download the extension directly from our repository:
|
||||
|
||||
- **Latest Production Release**: [tldraw-vscode.vsix](https://github.com/tldraw/tldraw/blob/production/apps/vscode/extension/release/tldraw-vscode.vsix) (from `production` branch)
|
||||
|
||||
To install from a `.vsix` file:
|
||||
|
||||
1. Download the file from the link above
|
||||
2. Open VS Code
|
||||
3. Go to Extensions (Ctrl+Shift+X)
|
||||
4. Click the "..." menu and select "Install from VSIX..."
|
||||
5. Select the downloaded `tldraw-vscode.vsix` file
|
||||
|
||||
## Contribution
|
||||
|
||||
Found a bug? Please [submit an issue](https://github.com/tldraw/tldraw/issues/new).
|
||||
|
||||
## License
|
||||
|
||||
This project is provided under the MIT license found [here](https://github.com/tldraw/tldraw/blob/main/apps/vscode/extension/LICENSE.md). The tldraw SDK is provided under the [tldraw license](https://github.com/tldraw/tldraw/blob/main/LICENSE.md).
|
||||
|
||||
## Trademarks
|
||||
|
||||
Copyright (c) 2024-present tldraw Inc. The tldraw name and logo are trademarks of tldraw. Please see our [trademark guidelines](https://github.com/tldraw/tldraw/blob/main/TRADEMARKS.md) for info on acceptable usage.
|
||||
|
||||
## Contact
|
||||
|
||||
Find us on Twitter/X at [@tldraw](https://twitter.com/tldraw).
|
||||
|
||||
## Community
|
||||
|
||||
Have questions, comments or feedback? [Join our discord](https://discord.tldraw.com/?utm_source=github&utm_medium=readme&utm_campaign=sociallink). For the latest news and release notes, visit [tldraw.dev](https://tldraw.dev).
|
||||
BIN
apps/vscode/extension/assets/vscode_open.png
Normal file
BIN
apps/vscode/extension/assets/vscode_open.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
168
apps/vscode/extension/examples/1.tldr
Normal file
168
apps/vscode/extension/examples/1.tldr
Normal file
@@ -0,0 +1,168 @@
|
||||
{
|
||||
"fileHandle": null,
|
||||
"document": {
|
||||
"id": "doc",
|
||||
"name": "New Document",
|
||||
"version": 15.3,
|
||||
"pages": {
|
||||
"page": {
|
||||
"id": "page",
|
||||
"name": "Page 1",
|
||||
"childIndex": 1,
|
||||
"shapes": {
|
||||
"3805a6e3-b50a-4bb2-0782-e687526cafc8": {
|
||||
"id": "3805a6e3-b50a-4bb2-0782-e687526cafc8",
|
||||
"type": "rectangle",
|
||||
"name": "Rectangle",
|
||||
"parentId": "page",
|
||||
"childIndex": 1,
|
||||
"point": [
|
||||
245.96,
|
||||
277.88
|
||||
],
|
||||
"size": [
|
||||
292.43,
|
||||
125.76
|
||||
],
|
||||
"rotation": 0,
|
||||
"style": {
|
||||
"color": "white",
|
||||
"size": "small",
|
||||
"isFilled": false,
|
||||
"dash": "draw"
|
||||
},
|
||||
"label": "Rectangles with labels"
|
||||
},
|
||||
"9a20ffa8-044b-4ca3-0448-632e2b2831d1": {
|
||||
"id": "9a20ffa8-044b-4ca3-0448-632e2b2831d1",
|
||||
"type": "ellipse",
|
||||
"name": "Ellipse",
|
||||
"parentId": "page",
|
||||
"childIndex": 3,
|
||||
"point": [
|
||||
995.24,
|
||||
235.11
|
||||
],
|
||||
"radius": [
|
||||
112.92500000000001,
|
||||
108.96000000000004
|
||||
],
|
||||
"rotation": 0,
|
||||
"style": {
|
||||
"color": "white",
|
||||
"size": "small",
|
||||
"isFilled": true,
|
||||
"dash": "draw",
|
||||
"scale": 1
|
||||
},
|
||||
"label": "Ovals with labels",
|
||||
"labelPoint": [
|
||||
0.5,
|
||||
0.5
|
||||
]
|
||||
},
|
||||
"c2125c4e-e9ef-4eeb-2011-9e8aaeadd6ea": {
|
||||
"id": "c2125c4e-e9ef-4eeb-2011-9e8aaeadd6ea",
|
||||
"type": "arrow",
|
||||
"name": "Arrow",
|
||||
"parentId": "page",
|
||||
"childIndex": 4,
|
||||
"point": [
|
||||
538.39,
|
||||
341.56
|
||||
],
|
||||
"rotation": 0,
|
||||
"bend": -0.000013369615345367926,
|
||||
"handles": {
|
||||
"start": {
|
||||
"id": "start",
|
||||
"index": 0,
|
||||
"point": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"canBind": true,
|
||||
"bindingId": "31007b26-4667-4932-00aa-7f389642b187"
|
||||
},
|
||||
"end": {
|
||||
"id": "end",
|
||||
"index": 1,
|
||||
"point": [
|
||||
448.85,
|
||||
2.44
|
||||
],
|
||||
"canBind": true,
|
||||
"bindingId": "7b099540-f8b1-4bf3-0d7d-2b74df370067"
|
||||
},
|
||||
"bend": {
|
||||
"id": "bend",
|
||||
"index": 2,
|
||||
"point": [
|
||||
224.43,
|
||||
1.22
|
||||
]
|
||||
}
|
||||
},
|
||||
"decorations": {
|
||||
"end": "arrow"
|
||||
},
|
||||
"style": {
|
||||
"color": "white",
|
||||
"size": "small",
|
||||
"isFilled": false,
|
||||
"dash": "draw",
|
||||
"scale": 1
|
||||
},
|
||||
"label": "Arrows with labels",
|
||||
"labelPoint": [
|
||||
0.5,
|
||||
0.5
|
||||
]
|
||||
}
|
||||
},
|
||||
"bindings": {
|
||||
"7b099540-f8b1-4bf3-0d7d-2b74df370067": {
|
||||
"id": "7b099540-f8b1-4bf3-0d7d-2b74df370067",
|
||||
"type": "arrow",
|
||||
"fromId": "c2125c4e-e9ef-4eeb-2011-9e8aaeadd6ea",
|
||||
"toId": "9a20ffa8-044b-4ca3-0448-632e2b2831d1",
|
||||
"handleId": "end",
|
||||
"point": [
|
||||
0.08,
|
||||
0.5
|
||||
],
|
||||
"distance": 8
|
||||
},
|
||||
"31007b26-4667-4932-00aa-7f389642b187": {
|
||||
"id": "31007b26-4667-4932-00aa-7f389642b187",
|
||||
"type": "arrow",
|
||||
"fromId": "c2125c4e-e9ef-4eeb-2011-9e8aaeadd6ea",
|
||||
"toId": "3805a6e3-b50a-4bb2-0782-e687526cafc8",
|
||||
"handleId": "start",
|
||||
"point": [
|
||||
0.5,
|
||||
0.5
|
||||
],
|
||||
"distance": 16
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"pageStates": {
|
||||
"page": {
|
||||
"id": "page",
|
||||
"selectedIds": [],
|
||||
"camera": {
|
||||
"point": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"zoom": 1
|
||||
},
|
||||
"editingId": null
|
||||
}
|
||||
},
|
||||
"assets": {}
|
||||
},
|
||||
"assets": {}
|
||||
}
|
||||
1026
apps/vscode/extension/examples/2.tldr
Normal file
1026
apps/vscode/extension/examples/2.tldr
Normal file
File diff suppressed because it is too large
Load Diff
1104
apps/vscode/extension/examples/3.tldr
Normal file
1104
apps/vscode/extension/examples/3.tldr
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
{"tldrawFileFormatVersion":1,"schema":{"schemaVersion":1,"storeVersion":1,"recordVersions":{"asset":{"version":0,"subTypeKey":"type","subTypeVersions":{"image":2,"video":2,"bookmark":0}},"camera":{"version":0},"document":{"version":0},"instance":{"version":5},"instance_page_state":{"version":0},"page":{"version":0},"shape":{"version":1,"subTypeKey":"type","subTypeVersions":{"draw":1,"text":0,"line":0,"arrow":1,"image":1,"video":1,"geo":2,"note":1,"group":0,"bookmark":1,"embed":1,"frame":0}},"user":{"version":0},"user_document":{"version":2},"user_presence":{"version":0}}},"records":[{"x":0,"y":0,"z":1,"id":"camera:ZEdonVzBw8UQNxHVSfYeN","typeName":"camera"},{"gridSize":10,"id":"document:document","typeName":"document"},{"propsForNextShape":{"opacity":"1","color":"black","labelColor":"black","dash":"draw","fill":"none","size":"m","icon":"file","font":"draw","align":"middle","geo":"rectangle","arrowheadStart":"none","arrowheadEnd":"arrow","spline":"line"},"brush":null,"scribble":null,"cursor":{"type":"default","color":"black","rotation":0},"isFocusMode":false,"exportBackground":true,"isDebugMode":true,"isToolLocked":false,"screenBounds":{"x":0,"y":0,"w":1016,"h":1308},"id":"instance:9NyQ7QvIoscZrrjfaLlw3","userId":"user:THIgzt9P6hTGullzaOPpd","currentPageId":"page:DldMYGg34nlqr9pTjmOaV","typeName":"instance"},{"editingId":null,"selectedIds":[],"hoveredId":null,"erasingIds":[],"hintingIds":[],"focusLayerId":null,"id":"instance_page_state:l05WL5JCaWeamtf3xX-rR","pageId":"page:DldMYGg34nlqr9pTjmOaV","instanceId":"instance:9NyQ7QvIoscZrrjfaLlw3","cameraId":"camera:ZEdonVzBw8UQNxHVSfYeN","typeName":"instance_page_state"},{"id":"page:DldMYGg34nlqr9pTjmOaV","name":"Page 1","index":"a1","typeName":"page"},{"name":"New User","locale":"en","id":"user:THIgzt9P6hTGullzaOPpd","typeName":"user"},{"isReadOnly":false,"isPenMode":false,"isGridMode":false,"isDarkMode":false,"isMobileMode":false,"isSnapMode":false,"lastUpdatedPageId":"page:DldMYGg34nlqr9pTjmOaV","lastUsedTabId":"instance:Bgu8sMzoEjBFOltNtawzR","id":"user_document:m48KwWKytfJTnajCKC4Ci","userId":"user:THIgzt9P6hTGullzaOPpd","typeName":"user_document"},{"lastUsedInstanceId":"instance:Bgu8sMzoEjBFOltNtawzR","lastActivityTimestamp":1679326918108,"cursor":{"x":1057.125,"y":439.03125,"z":0},"color":"#39B178","id":"user_presence:mWjDj8J3KWsxO9FST9mJY","userId":"user:THIgzt9P6hTGullzaOPpd","typeName":"user_presence"},{"propsForNextShape":{"opacity":"1","color":"black","labelColor":"black","dash":"draw","fill":"none","size":"m","icon":"file","font":"draw","align":"middle","geo":"rectangle","arrowheadStart":"none","arrowheadEnd":"arrow","spline":"line"},"brush":null,"scribble":null,"cursor":{"type":"default","color":"black","rotation":0},"isFocusMode":false,"exportBackground":true,"isDebugMode":true,"isToolLocked":false,"screenBounds":{"x":0,"y":0,"w":1016,"h":1308},"id":"instance:qjCFl-gVEHZV6qnavaI5A","userId":"user:THIgzt9P6hTGullzaOPpd","currentPageId":"page:DldMYGg34nlqr9pTjmOaV","typeName":"instance"},{"x":0,"y":0,"z":1,"id":"camera:kDPjsRu3p8aoVfzWbNISY","typeName":"camera"},{"editingId":null,"selectedIds":[],"hoveredId":null,"erasingIds":[],"hintingIds":[],"focusLayerId":null,"id":"instance_page_state:80sg16r6TLoBdklDRg4px","pageId":"page:DldMYGg34nlqr9pTjmOaV","instanceId":"instance:qjCFl-gVEHZV6qnavaI5A","cameraId":"camera:kDPjsRu3p8aoVfzWbNISY","typeName":"instance_page_state"},{"propsForNextShape":{"opacity":"1","color":"black","labelColor":"black","dash":"draw","fill":"none","size":"m","icon":"file","font":"draw","align":"middle","geo":"rectangle","arrowheadStart":"none","arrowheadEnd":"arrow","spline":"line"},"brush":null,"scribble":null,"cursor":{"type":"default","color":"black","rotation":0},"isFocusMode":false,"exportBackground":true,"isDebugMode":true,"isToolLocked":false,"screenBounds":{"x":0,"y":0,"w":1223,"h":1308},"id":"instance:Bgu8sMzoEjBFOltNtawzR","userId":"user:THIgzt9P6hTGullzaOPpd","currentPageId":"page:DldMYGg34nlqr9pTjmOaV","typeName":"instance"},{"x":0,"y":0,"z":1,"id":"camera:6grbCQvcD63BvbkYvT_zD","typeName":"camera"},{"editingId":null,"selectedIds":["shape:qzz1TZBX_0FfNiKrwE8pI"],"hoveredId":null,"erasingIds":[],"hintingIds":[],"focusLayerId":null,"id":"instance_page_state:VIw9Cdw1hWs5ABt9Bpl8U","pageId":"page:DldMYGg34nlqr9pTjmOaV","instanceId":"instance:Bgu8sMzoEjBFOltNtawzR","cameraId":"camera:6grbCQvcD63BvbkYvT_zD","typeName":"instance_page_state"},{"x":454.046875,"y":384.203125,"rotation":0,"isLocked":false,"id":"shape:qzz1TZBX_0FfNiKrwE8pI","type":"geo","props":{"w":97.83984375,"h":103.23828125,"geo":"rectangle","color":"black","labelColor":"black","fill":"none","dash":"draw","size":"m","opacity":"1","font":"draw","text":"","align":"middle","growY":0,"url":""},"parentId":"page:DldMYGg34nlqr9pTjmOaV","index":"a1","typeName":"unknown-type"}]}
|
||||
@@ -0,0 +1 @@
|
||||
This is not a valid tldr file.
|
||||
@@ -0,0 +1 @@
|
||||
{"tldrawFileFormatVersion":1000,"schema":{"schemaVersion":1,"storeVersion":1,"recordVersions":{"asset":{"version":0,"subTypeKey":"type","subTypeVersions":{"image":2,"video":2,"bookmark":0}},"camera":{"version":0},"document":{"version":0},"instance":{"version":5},"instance_page_state":{"version":0},"page":{"version":0},"user":{"version":0},"user_document":{"version":2},"user_presence":{"version":0},"shape":{"version":1,"subTypeKey":"type","subTypeVersions":{"draw":1,"text":0,"line":0,"arrow":1,"image":1,"video":1,"geo":2,"note":1,"group":0,"bookmark":1,"embed":1,"frame":0}}}},"records":[{"gridSize":10,"id":"document:document","typeName":"document"},{"isReadOnly":false,"isPenMode":false,"isGridMode":false,"isDarkMode":false,"isMobileMode":false,"isSnapMode":false,"lastUpdatedPageId":"page:wTYNt8AJ-CAe-ozU4bYw0","lastUsedTabId":"instance:H7Ghe-kI0V36JYl-FtXV6","id":"user_document:384AkIr8uUukpLs33G6TO","userId":"user:P_x7HuwQNs0er568NA5nt","typeName":"user_document"},{"id":"page:wTYNt8AJ-CAe-ozU4bYw0","name":"Page 1","index":"a1","typeName":"page"},{"propsForNextShape":{"opacity":"1","color":"black","labelColor":"black","dash":"draw","fill":"none","size":"m","icon":"file","font":"draw","align":"middle","geo":"rectangle","arrowheadStart":"none","arrowheadEnd":"arrow","spline":"line"},"brush":null,"scribble":null,"cursor":{"type":"default","color":"black","rotation":0},"isFocusMode":false,"exportBackground":true,"isDebugMode":false,"isToolLocked":false,"screenBounds":{"x":0,"y":0,"w":1920,"h":1093},"id":"instance:H7Ghe-kI0V36JYl-FtXV6","userId":"user:P_x7HuwQNs0er568NA5nt","currentPageId":"page:wTYNt8AJ-CAe-ozU4bYw0","typeName":"instance"},{"name":"New User","locale":"en","id":"user:P_x7HuwQNs0er568NA5nt","typeName":"user"},{"lastUsedInstanceId":"instance:H7Ghe-kI0V36JYl-FtXV6","lastActivityTimestamp":1679326696474,"cursor":{"x":1218.24609375,"y":608.05859375,"z":0},"color":"#55B467","id":"user_presence:mao_e5ejuI4IdRbJPALvk","userId":"user:P_x7HuwQNs0er568NA5nt","typeName":"user_presence"},{"x":0,"y":0,"z":1,"id":"camera:EJQijEXfj0inf8U-V-cUp","typeName":"camera"},{"editingId":null,"selectedIds":["shape:is_pvm9FlmAIZ2IYg-9jp"],"hoveredId":null,"erasingIds":[],"hintingIds":[],"focusLayerId":null,"id":"instance_page_state:xcQsboc8bb8EOAh_0wA2u","pageId":"page:wTYNt8AJ-CAe-ozU4bYw0","instanceId":"instance:H7Ghe-kI0V36JYl-FtXV6","cameraId":"camera:EJQijEXfj0inf8U-V-cUp","typeName":"instance_page_state"},{"x":696.625,"y":390.11328125,"rotation":0,"isLocked":false,"id":"shape:is_pvm9FlmAIZ2IYg-9jp","type":"geo","props":{"w":174.56640625,"h":209.875,"geo":"rectangle","color":"black","labelColor":"black","fill":"none","dash":"draw","size":"m","opacity":"1","font":"draw","text":"","align":"middle","growY":0,"url":""},"parentId":"page:wTYNt8AJ-CAe-ozU4bYw0","index":"a1","typeName":"shape"}]}
|
||||
3941
apps/vscode/extension/examples/v2.tldr
Normal file
3941
apps/vscode/extension/examples/v2.tldr
Normal file
File diff suppressed because it is too large
Load Diff
3231
apps/vscode/extension/examples/v2a.tldr
Normal file
3231
apps/vscode/extension/examples/v2a.tldr
Normal file
File diff suppressed because it is too large
Load Diff
BIN
apps/vscode/extension/icon.png
Normal file
BIN
apps/vscode/extension/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.4 KiB |
180
apps/vscode/extension/package.json
Normal file
180
apps/vscode/extension/package.json
Normal file
@@ -0,0 +1,180 @@
|
||||
{
|
||||
"name": "tldraw-vscode",
|
||||
"description": "The tldraw extension for VS Code.",
|
||||
"version": "2.0.36",
|
||||
"private": true,
|
||||
"author": {
|
||||
"name": "tldraw Inc.",
|
||||
"email": "hello@tldraw.com"
|
||||
},
|
||||
"license": "MIT",
|
||||
"homepage": "https://tldraw.dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/tldraw/tldraw"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/tldraw/tldraw/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"tldraw",
|
||||
"sdk",
|
||||
"drawing",
|
||||
"app",
|
||||
"development",
|
||||
"whiteboard",
|
||||
"canvas",
|
||||
"infinite"
|
||||
],
|
||||
"displayName": "tldraw",
|
||||
"publisher": "tldraw-org",
|
||||
"icon": "icon.png",
|
||||
"galleryBanner": {
|
||||
"color": "#1d1d1d",
|
||||
"theme": "dark"
|
||||
},
|
||||
"categories": [
|
||||
"Visualization"
|
||||
],
|
||||
"engines": {
|
||||
"vscode": "^1.96.0"
|
||||
},
|
||||
"activationEvents": [],
|
||||
"browser": "./dist/web/extension.js",
|
||||
"main": "./dist/web/extension.js",
|
||||
"extensionKind": [
|
||||
"workspace"
|
||||
],
|
||||
"contributes": {
|
||||
"customEditors": [
|
||||
{
|
||||
"viewType": "tldraw.tldr",
|
||||
"displayName": "tldraw",
|
||||
"selector": [
|
||||
{
|
||||
"filenamePattern": "*.tldr"
|
||||
},
|
||||
{
|
||||
"filenamePattern": "*.tldr.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"viewType": "tldraw.tldraw-offline",
|
||||
"displayName": "tldraw",
|
||||
"selector": [
|
||||
{
|
||||
"filenamePattern": "*.tldraw"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"keybindings": [
|
||||
{
|
||||
"key": "ctrl+shift+d",
|
||||
"mac": "cmd+shift+d",
|
||||
"title": "Toggle Dark Mode",
|
||||
"command": "tldraw.tldr.toggleDarkMode",
|
||||
"category": "tldraw",
|
||||
"when": "resourceExtname == .tldr"
|
||||
},
|
||||
{
|
||||
"key": "ctrl+numpad_add",
|
||||
"mac": "cmd+numpad_add",
|
||||
"title": "Zoom In",
|
||||
"command": "tldraw.tldr.zoomIn",
|
||||
"category": "tldraw",
|
||||
"when": "resourceExtname == .tldr"
|
||||
},
|
||||
{
|
||||
"key": "ctrl+=",
|
||||
"mac": "cmd+=",
|
||||
"title": "Zoom In",
|
||||
"command": "tldraw.tldr.zoomIn",
|
||||
"category": "tldraw",
|
||||
"when": "resourceExtname == .tldr"
|
||||
},
|
||||
{
|
||||
"key": "ctrl+numpad_subtract",
|
||||
"mac": "cmd+numpad_subtract",
|
||||
"title": "Zoom Out",
|
||||
"command": "tldraw.tldr.zoomOut",
|
||||
"category": "tldraw",
|
||||
"when": "resourceExtname == .tldr"
|
||||
},
|
||||
{
|
||||
"key": "ctrl+-",
|
||||
"mac": "cmd+-",
|
||||
"title": "Zoom Out",
|
||||
"command": "tldraw.tldr.zoomOut",
|
||||
"category": "tldraw",
|
||||
"when": "resourceExtname == .tldr"
|
||||
},
|
||||
{
|
||||
"key": "ctrl+numpad0",
|
||||
"mac": "cmd+numpad0",
|
||||
"title": "Reset Zoom",
|
||||
"command": "tldraw.tldr.resetZoom",
|
||||
"category": "tldraw",
|
||||
"when": "resourceExtname == .tldr"
|
||||
}
|
||||
],
|
||||
"commands": [
|
||||
{
|
||||
"command": "tldraw.tldr.new",
|
||||
"title": "New Project",
|
||||
"category": "tldraw"
|
||||
}
|
||||
]
|
||||
},
|
||||
"vsce": {
|
||||
"dependencies": false
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "tsx scripts/dev.ts",
|
||||
"build": "cd ../editor && yarn build && cd ../extension && tsx scripts/build.ts",
|
||||
"get-info": "vsce show tldraw-org.tldraw-vscode --json > extension.json",
|
||||
"package": "yarn build && tsx scripts/package.ts",
|
||||
"publish": "tsx scripts/publish.ts",
|
||||
"lint": "yarn run -T tsx ../../../internal/scripts/lint.ts",
|
||||
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist && rm -rf editor && rm -rf temp & yarn",
|
||||
"test": "yarn run -T vitest --passWithNoTests",
|
||||
"test-ci": "yarn run -T vitest run --passWithNoTests",
|
||||
"test-coverage": "yarn run -T vitest run --coverage --passWithNoTests"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tldraw/editor": "workspace:*",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/lodash": "^4.17.14",
|
||||
"@types/node-fetch": "^2.6.12",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/vscode": "^1.96.0",
|
||||
"assert": "^2.1.0",
|
||||
"esbuild": "^0.28.0",
|
||||
"fs-extra": "^11.3.0",
|
||||
"lazyrepo": "0.0.0-alpha.27",
|
||||
"lodash": "^4.17.21",
|
||||
"mocha": "^9.2.2",
|
||||
"ovsx": "^0.10.7",
|
||||
"process": "^0.11.10",
|
||||
"react": "^19.2.1",
|
||||
"react-dom": "^19.2.1",
|
||||
"tldraw": "workspace:*",
|
||||
"ts-loader": "^9.5.2",
|
||||
"tslib": "^2.8.1",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^7.0.2",
|
||||
"vsce": "^2.15.0",
|
||||
"webpack": "^5.97.1"
|
||||
},
|
||||
"gitHead": "4b1137849ad07da36fc8f0f19cb64e7535a79296",
|
||||
"dependencies": {
|
||||
"cheerio": "^1.0.0",
|
||||
"node-fetch": "^2.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.2.0 || ^19.2.1",
|
||||
"react-dom": "^18.2.0 || ^19.2.1"
|
||||
}
|
||||
}
|
||||
42
apps/vscode/extension/scripts/build.ts
Normal file
42
apps/vscode/extension/scripts/build.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import esbuild from 'esbuild'
|
||||
import { logEnv } from './cli'
|
||||
import { copyEditor, removeDistDirectory } from './helpers'
|
||||
|
||||
const log = logEnv('extension')
|
||||
|
||||
async function build() {
|
||||
await copyEditor({ log })
|
||||
await removeDistDirectory({ log })
|
||||
|
||||
try {
|
||||
const entryPoints = ['./src/extension.ts']
|
||||
log({ cmd: 'esbuild', args: { entryPoints } })
|
||||
await esbuild.build({
|
||||
entryPoints,
|
||||
outdir: 'dist/web',
|
||||
minify: false,
|
||||
bundle: true,
|
||||
format: 'cjs',
|
||||
target: 'es6',
|
||||
platform: 'node',
|
||||
define: {
|
||||
'process.env.NODE_ENV': '"production"',
|
||||
},
|
||||
tsconfig: './tsconfig.json',
|
||||
external: ['vscode'],
|
||||
loader: {
|
||||
'.woff2': 'dataurl',
|
||||
'.woff': 'dataurl',
|
||||
'.svg': 'file',
|
||||
'.png': 'file',
|
||||
'.json': 'file',
|
||||
},
|
||||
})
|
||||
|
||||
log({ cmd: 'esbuild:success', args: { entryPoints } })
|
||||
} catch (error) {
|
||||
log({ cmd: 'esbuild:error', args: { error } })
|
||||
throw error
|
||||
}
|
||||
}
|
||||
build()
|
||||
58
apps/vscode/extension/scripts/cli.ts
Normal file
58
apps/vscode/extension/scripts/cli.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import path from 'path'
|
||||
|
||||
const displayRelative = (from: string, to: string) => {
|
||||
const outpath = path.relative(from, to)
|
||||
if (!outpath.match(/^\./)) {
|
||||
return `./${outpath}`
|
||||
}
|
||||
return outpath
|
||||
}
|
||||
|
||||
type LogDef =
|
||||
| { cmd: 'remove'; env: string; args: { target: string } }
|
||||
| { cmd: 'copy'; env: string; args: { source: string; dest: string } }
|
||||
| { cmd: 'esbuild'; env: string; args: { entryPoints: string[] } }
|
||||
| { cmd: 'esbuild:success'; env: string; args: any }
|
||||
| { cmd: 'esbuild:error'; env: string; args: { error: string } }
|
||||
| { cmd: 'esbuild:serve'; env: string; args: { host: string; port: number | string } }
|
||||
|
||||
export function log(def: LogDef) {
|
||||
const printStderr = (icon: string, cmd: string, ...args: unknown[]) => {
|
||||
console.error(`${icon} [${def.env ?? 'unknown'}/${cmd}]`, ...args)
|
||||
}
|
||||
|
||||
if (def.cmd === 'remove') {
|
||||
const { target } = def.args
|
||||
printStderr('🗑 ', 'remove', displayRelative(process.cwd(), target))
|
||||
} else if (def.cmd === 'copy') {
|
||||
const { source, dest } = def.args
|
||||
printStderr(
|
||||
'🏠',
|
||||
'copy',
|
||||
`${displayRelative(process.cwd(), source)} -> ${displayRelative(process.cwd(), dest)}`
|
||||
)
|
||||
} else if (def.cmd === 'esbuild') {
|
||||
printStderr(
|
||||
'🤖',
|
||||
'esbuild',
|
||||
`${def.args.entryPoints.map((pathname) => displayRelative(process.cwd(), pathname))}`
|
||||
)
|
||||
} else if (def.cmd === 'esbuild:success') {
|
||||
printStderr('✅', `esbuild`, `build successful (${new Date().toISOString()})`)
|
||||
} else if (def.cmd === 'esbuild:error') {
|
||||
printStderr(`❌`, `esbuild`, `error`)
|
||||
console.error(def.args.error)
|
||||
} else if (def.cmd === 'esbuild:serve') {
|
||||
const { host = 'localhost', port } = def.args
|
||||
printStderr(`🌎`, `esbuild`, `serving <http://${host}:${port}>`)
|
||||
} else {
|
||||
// @ts-ignore
|
||||
printStderr(`❓`, def.cmd, JSON.stringify(def.args))
|
||||
}
|
||||
}
|
||||
|
||||
export function logEnv(env: string) {
|
||||
return (opts: any) => {
|
||||
log({ ...opts, env })
|
||||
}
|
||||
}
|
||||
60
apps/vscode/extension/scripts/dev.ts
Normal file
60
apps/vscode/extension/scripts/dev.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { join } from 'path'
|
||||
import esbuild from 'esbuild'
|
||||
import { logEnv } from './cli'
|
||||
import { copyEditor, removeDistDirectory } from './helpers'
|
||||
import { getDirname } from './path'
|
||||
|
||||
const rootDir = getDirname(import.meta.url, '../')
|
||||
const log = logEnv('extension')
|
||||
|
||||
async function dev() {
|
||||
await copyEditor({ log })
|
||||
await removeDistDirectory({ log })
|
||||
const entryPoints = [join(rootDir, 'src', 'extension.ts')]
|
||||
|
||||
log({ cmd: 'esbuild', args: { entryPoints } })
|
||||
try {
|
||||
const builder = await esbuild.context({
|
||||
entryPoints,
|
||||
outdir: join(rootDir, 'dist', 'web'),
|
||||
minify: false,
|
||||
bundle: true,
|
||||
format: 'cjs',
|
||||
target: 'es6',
|
||||
sourcemap: 'inline',
|
||||
platform: 'node',
|
||||
define: {
|
||||
'process.env.NODE_ENV': '"development"',
|
||||
},
|
||||
tsconfig: './tsconfig.json',
|
||||
external: ['vscode'],
|
||||
plugins: [
|
||||
{
|
||||
name: 'log-builds',
|
||||
setup(build) {
|
||||
build.onEnd((result) => {
|
||||
if (result.errors.length) {
|
||||
log({ cmd: 'esbuild:error', args: { err: result.errors } })
|
||||
} else {
|
||||
copyEditor({ log })
|
||||
log({ cmd: 'esbuild:success', args: {} })
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
],
|
||||
loader: {
|
||||
'.woff2': 'dataurl',
|
||||
'.woff': 'dataurl',
|
||||
'.svg': 'file',
|
||||
'.png': 'file',
|
||||
'.json': 'file',
|
||||
},
|
||||
})
|
||||
await builder.watch()
|
||||
} catch (error) {
|
||||
log({ cmd: 'esbuild:error', args: { error } })
|
||||
throw error
|
||||
}
|
||||
}
|
||||
dev()
|
||||
25
apps/vscode/extension/scripts/helpers.ts
Normal file
25
apps/vscode/extension/scripts/helpers.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import fs from 'fs'
|
||||
import { join } from 'path'
|
||||
import fse from 'fs-extra'
|
||||
import { exists, getDirname } from './path'
|
||||
|
||||
const vscodeDir = getDirname(import.meta.url, '../../')
|
||||
|
||||
export async function copyEditor({ log }: { log(opts: any): void }) {
|
||||
const editorRoot = join(vscodeDir, 'editor')
|
||||
const extensionRoot = join(vscodeDir, 'extension')
|
||||
|
||||
const source = join(editorRoot, 'dist')
|
||||
const dest = join(extensionRoot, 'editor')
|
||||
|
||||
log({ cmd: 'copy', args: { source, dest } })
|
||||
await fse.copy(source, dest)
|
||||
}
|
||||
|
||||
export async function removeDistDirectory({ log }: { log(opts: any): void }) {
|
||||
const target = join(vscodeDir, 'extension', 'dist')
|
||||
if (await exists(target)) {
|
||||
log({ cmd: 'remove', args: { target } })
|
||||
await fs.promises.rm(target, { recursive: true })
|
||||
}
|
||||
}
|
||||
38
apps/vscode/extension/scripts/package.ts
Normal file
38
apps/vscode/extension/scripts/package.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { exec } from 'child_process'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
|
||||
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf8'))
|
||||
|
||||
const { log: jslog } = console
|
||||
|
||||
async function main() {
|
||||
if (fs.existsSync('./editor')) {
|
||||
fs.rmSync('./editor', { recursive: true })
|
||||
}
|
||||
if (fs.existsSync('./temp')) {
|
||||
fs.rmSync('./temp', { recursive: true })
|
||||
}
|
||||
|
||||
fs.mkdirSync('./temp')
|
||||
|
||||
const preRelease = process.argv.includes('--pre-release')
|
||||
try {
|
||||
exec(
|
||||
`cp -r ../editor/dist editor; vsce package${preRelease ? ' --pre-release' : ''}; mv ${pkg.name}-${pkg.version}.vsix ${'./temp'}`,
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
throw new Error(error.message)
|
||||
}
|
||||
if (stderr && !/^(warning|\(node:)/.test(stderr)) {
|
||||
throw new Error(stderr)
|
||||
}
|
||||
}
|
||||
)
|
||||
} catch (e) {
|
||||
jslog(`× ${pkg.name}: Build failed due to an error.`)
|
||||
jslog(e)
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
16
apps/vscode/extension/scripts/path.ts
Normal file
16
apps/vscode/extension/scripts/path.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
export function getDirname(metaUrl: string, targetPath: string) {
|
||||
const dirname = path.dirname(metaUrl.replace('file://', ''))
|
||||
return path.normalize(path.join(dirname, targetPath))
|
||||
}
|
||||
|
||||
export async function exists(targetFolder: string) {
|
||||
try {
|
||||
await fs.promises.access(targetFolder)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
82
apps/vscode/extension/scripts/publish.ts
Normal file
82
apps/vscode/extension/scripts/publish.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { exec } from 'child_process'
|
||||
import { readdirSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
import { promisify } from 'util'
|
||||
|
||||
const execAsync = promisify(exec)
|
||||
const MAX_REGISTRY_PUBLISH_ATTEMPTS = 3
|
||||
const REGISTRY_PUBLISH_RETRY_DELAY_MS = 10_000
|
||||
|
||||
class AmbiguousPublishError extends Error {}
|
||||
|
||||
async function publishWithRetry(command: string) {
|
||||
for (let attempt = 1; attempt <= MAX_REGISTRY_PUBLISH_ATTEMPTS; attempt++) {
|
||||
try {
|
||||
await execAsync(command)
|
||||
return
|
||||
} catch (err) {
|
||||
const error = err as Error & { stdout?: string; stderr?: string }
|
||||
if (
|
||||
[error.message, error.stdout, error.stderr].some((text) => text?.includes('already exists'))
|
||||
) {
|
||||
if (attempt > 1) {
|
||||
throw new AmbiguousPublishError('An earlier publish attempt may have succeeded.', {
|
||||
cause: err,
|
||||
})
|
||||
}
|
||||
throw err
|
||||
}
|
||||
if (attempt === MAX_REGISTRY_PUBLISH_ATTEMPTS) {
|
||||
throw err
|
||||
}
|
||||
console.error(
|
||||
`Publish attempt ${attempt}/${MAX_REGISTRY_PUBLISH_ATTEMPTS} failed; retrying in ${REGISTRY_PUBLISH_RETRY_DELAY_MS / 1000}s...`,
|
||||
err
|
||||
)
|
||||
await new Promise((resolve) => setTimeout(resolve, REGISTRY_PUBLISH_RETRY_DELAY_MS))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getVsixPath(): string {
|
||||
const tempDir = join(__dirname, '../temp')
|
||||
const files = readdirSync(tempDir)
|
||||
const vsixFile = files.find((file) => file.endsWith('.vsix'))
|
||||
if (!vsixFile) {
|
||||
throw new Error('No .vsix file found in temp directory')
|
||||
}
|
||||
return join(tempDir, vsixFile)
|
||||
}
|
||||
|
||||
async function publishToVSCodeMarketplace(preRelease: boolean) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Publishing to VS Code Marketplace${preRelease ? ' (pre-release)' : ''}`)
|
||||
await publishWithRetry(`vsce publish${preRelease ? ' --pre-release' : ''}`)
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Successfully published to VS Code Marketplace')
|
||||
}
|
||||
|
||||
async function publishToOpenVSX(preRelease: boolean) {
|
||||
const vsixPath = getVsixPath()
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Publishing to Open VSX...')
|
||||
// An upload can succeed even if its response is lost; retrying must accept that version.
|
||||
await publishWithRetry(
|
||||
`npx ovsx publish --skip-duplicate${preRelease ? ' --pre-release' : ''} ${vsixPath}`
|
||||
)
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Successfully published to Open VSX')
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const preRelease = process.argv.includes('--pre-release')
|
||||
|
||||
await publishToVSCodeMarketplace(preRelease)
|
||||
await publishToOpenVSX(preRelease)
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err)
|
||||
// The outer publisher must not interpret the duplicate in the cause as a version conflict.
|
||||
process.exit(err instanceof AmbiguousPublishError ? 75 : 1)
|
||||
})
|
||||
181
apps/vscode/extension/src/TldrawDocument.ts
Normal file
181
apps/vscode/extension/src/TldrawDocument.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import { TldrawFile } from 'tldraw'
|
||||
import * as vscode from 'vscode'
|
||||
import { defaultFileContents, fileExists, loadFile } from './file'
|
||||
import { nicelog } from './utils'
|
||||
|
||||
export type DocumentChangeEventArgs =
|
||||
| { reason: 'undo' | 'redo' }
|
||||
| {
|
||||
reason: 'revert'
|
||||
fileContents: TldrawFile
|
||||
}
|
||||
export class TLDrawDocument implements vscode.CustomDocument {
|
||||
isBlankDocument = false
|
||||
lastBackupDestination: vscode.Uri | undefined
|
||||
|
||||
private readonly _onDidChangeDocument = new vscode.EventEmitter<DocumentChangeEventArgs>()
|
||||
|
||||
/** Fired to notify webviews that the document has changed. */
|
||||
public readonly onDidChangeContent = this._onDidChangeDocument.event
|
||||
|
||||
private readonly _onDidChange = new vscode.EventEmitter<{
|
||||
readonly label: string
|
||||
undo(): void
|
||||
redo(): void
|
||||
}>()
|
||||
|
||||
public readonly onDidChange = this._onDidChange.event
|
||||
|
||||
private readonly _onDidDispose = new vscode.EventEmitter<void>()
|
||||
/** Fired when the document is disposed of. */
|
||||
public readonly onDidDispose = this._onDidDispose.event
|
||||
|
||||
private disposables: vscode.Disposable[] = [
|
||||
this._onDidChange,
|
||||
this._onDidChangeDocument,
|
||||
this._onDidDispose,
|
||||
]
|
||||
|
||||
private constructor(
|
||||
public uri: vscode.Uri,
|
||||
public documentData: TldrawFile,
|
||||
backupId: string | undefined
|
||||
) {
|
||||
this.isBlankDocument = backupId === 'undefined'
|
||||
}
|
||||
|
||||
static async create(uri: vscode.Uri, backupId: string | undefined) {
|
||||
let fileData: TldrawFile
|
||||
if (typeof backupId === 'string' && (await fileExists(vscode.Uri.parse(backupId)))) {
|
||||
fileData = await TLDrawDocument.readFile(vscode.Uri.parse(backupId))
|
||||
} else {
|
||||
fileData = await TLDrawDocument.readFile(uri)
|
||||
}
|
||||
return new TLDrawDocument(uri, fileData, backupId)
|
||||
}
|
||||
|
||||
makeEdit(nextFile: TldrawFile) {
|
||||
nicelog('makeEdit')
|
||||
const prevData = this.documentData
|
||||
this.documentData = nextFile
|
||||
this._onDidChange.fire({
|
||||
label: 'edit',
|
||||
undo: async () => {
|
||||
nicelog('undo')
|
||||
this.documentData = prevData
|
||||
this._onDidChangeDocument.fire({
|
||||
reason: 'undo',
|
||||
})
|
||||
},
|
||||
redo: async () => {
|
||||
nicelog('redo')
|
||||
this.documentData = nextFile
|
||||
this._onDidChangeDocument.fire({
|
||||
reason: 'redo',
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
dispose(): void {
|
||||
this.disposables.forEach((d) => d.dispose())
|
||||
this._onDidDispose.fire()
|
||||
}
|
||||
|
||||
private static async readFile(uri: vscode.Uri): Promise<TldrawFile> {
|
||||
nicelog('readFile')
|
||||
|
||||
if (uri.scheme === 'untitled') {
|
||||
return defaultFileContents
|
||||
}
|
||||
const fileContents = await vscode.workspace.fs.readFile(uri)
|
||||
return loadFile(Buffer.from(fileContents).toString('utf8'))
|
||||
}
|
||||
|
||||
async loadBlankDocument() {
|
||||
this.documentData = defaultFileContents
|
||||
await this.writeToResource(this.uri)
|
||||
if (this.lastBackupDestination && (await fileExists(this.lastBackupDestination))) {
|
||||
await vscode.workspace.fs.delete(this.lastBackupDestination)
|
||||
}
|
||||
}
|
||||
|
||||
/** Called by VS Code when the user saves the document. */
|
||||
async save(cancellation: vscode.CancellationToken): Promise<void> {
|
||||
nicelog('save')
|
||||
await this.saveAs(this.uri, cancellation)
|
||||
}
|
||||
|
||||
/** Called by VS Code when the user saves the document to a new location. */
|
||||
async saveAs(targetResource: vscode.Uri, cancellation: vscode.CancellationToken): Promise<void> {
|
||||
nicelog('saveAs')
|
||||
if (cancellation.isCancellationRequested) {
|
||||
return
|
||||
}
|
||||
await this.writeToResource(targetResource)
|
||||
}
|
||||
|
||||
private async writeToResource(targetResource: vscode.Uri) {
|
||||
const fileContents = Buffer.from(JSON.stringify(this.documentData, null, '\t'), 'utf8')
|
||||
await vscode.workspace.fs.writeFile(targetResource, fileContents)
|
||||
}
|
||||
|
||||
/** Called by VS Code when the user calls `revert` on a document. */
|
||||
async revert(_cancellation: vscode.CancellationToken): Promise<void> {
|
||||
nicelog('revert')
|
||||
|
||||
const diskContent = await TLDrawDocument.readFile(this.uri)
|
||||
this.documentData = diskContent
|
||||
this._onDidChangeDocument.fire({
|
||||
reason: 'revert',
|
||||
fileContents: diskContent,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by VS Code to backup the edited document.
|
||||
*
|
||||
* These backups are used to implement hot exit.
|
||||
*/
|
||||
async backup(
|
||||
destination: vscode.Uri,
|
||||
cancellation: vscode.CancellationToken
|
||||
): Promise<vscode.CustomDocumentBackup> {
|
||||
nicelog('backup')
|
||||
this.lastBackupDestination = destination
|
||||
|
||||
await this.saveAs(destination, cancellation)
|
||||
|
||||
return {
|
||||
id: destination.toString(),
|
||||
delete: async () => {
|
||||
try {
|
||||
await vscode.workspace.fs.delete(destination)
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async v1Backup(backupSaved: string, backupFailed: string) {
|
||||
const regex = /\.tldr$/gi
|
||||
if (!regex.test(this.uri.path)) {
|
||||
vscode.window.showInformationMessage(backupFailed)
|
||||
return
|
||||
}
|
||||
|
||||
let destination = this.uri.with({ path: this.uri.path.replace(/\.tldr$/gi, ' - old.tldr') })
|
||||
let exists = await fileExists(destination)
|
||||
let fileNumber = 1
|
||||
while (exists) {
|
||||
destination = this.uri.with({
|
||||
path: this.uri.path.replace(/\.tldr$/gi, ` - old (${fileNumber}).tldr`),
|
||||
})
|
||||
exists = await fileExists(destination)
|
||||
fileNumber++
|
||||
}
|
||||
await vscode.workspace.fs.copy(this.uri, destination, { overwrite: false })
|
||||
const fileName = destination.path.split('/').pop()
|
||||
vscode.window.showInformationMessage(`${backupSaved}: ${fileName}`)
|
||||
}
|
||||
}
|
||||
153
apps/vscode/extension/src/TldrawEditorProvider.ts
Normal file
153
apps/vscode/extension/src/TldrawEditorProvider.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import * as vscode from 'vscode'
|
||||
// @ts-ignore
|
||||
import type { VscodeMessage } from '../../messages'
|
||||
import { DocumentChangeEventArgs, TLDrawDocument } from './TldrawDocument'
|
||||
import { TldrawWebviewManager } from './TldrawWebviewManager'
|
||||
import { nicelog } from './utils'
|
||||
|
||||
export class TldrawEditorProvider implements vscode.CustomEditorProvider<TLDrawDocument> {
|
||||
private static newTDFileId = 1
|
||||
private disposables: vscode.Disposable[] = []
|
||||
private static readonly viewType = 'tldraw.tldr'
|
||||
private webviewPanels: vscode.WebviewPanel[] = []
|
||||
|
||||
private readonly _onDidChangeCustomDocument = new vscode.EventEmitter<
|
||||
vscode.CustomDocumentEditEvent<TLDrawDocument>
|
||||
>()
|
||||
public readonly onDidChangeCustomDocument = this._onDidChangeCustomDocument.event
|
||||
|
||||
constructor(private readonly context: vscode.ExtensionContext) {}
|
||||
|
||||
public static register(context: vscode.ExtensionContext): vscode.Disposable {
|
||||
// Several commands exist only to prevent the default keyboard shortcuts
|
||||
const noopCmds = ['zoomIn', 'zoomOut', 'resetZoom', 'toggleDarkMode']
|
||||
noopCmds.forEach((name) =>
|
||||
context.subscriptions.push(vscode.commands.registerCommand(`tldraw.tldr.${name}`, () => null))
|
||||
)
|
||||
|
||||
// Register the 'Create New File' command, which creates a temporary
|
||||
// .tldr file and opens it in the editor.
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(`tldraw.tldr.new`, () => {
|
||||
const id = this.newTDFileId++
|
||||
const name = id > 1 ? `New Document ${id}.tldr` : `New Document.tldr`
|
||||
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders
|
||||
const path = workspaceFolders ? workspaceFolders[0].uri : vscode.Uri.parse('')
|
||||
|
||||
vscode.commands.executeCommand(
|
||||
'vscode.openWith',
|
||||
vscode.Uri.joinPath(path, name).with({ scheme: 'untitled' }),
|
||||
this.viewType
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
// Register our editor provider, indicating to VS Code that we can
|
||||
// handle files with the .tldr extension.
|
||||
return vscode.window.registerCustomEditorProvider(
|
||||
this.viewType,
|
||||
new TldrawEditorProvider(context),
|
||||
{
|
||||
webviewOptions: {
|
||||
retainContextWhenHidden: true,
|
||||
},
|
||||
supportsMultipleEditorsPerDocument: true,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async openCustomDocument(
|
||||
uri: vscode.Uri,
|
||||
openContext: { backupId?: string },
|
||||
_token: vscode.CancellationToken
|
||||
): Promise<TLDrawDocument> {
|
||||
nicelog('openCustomDocument')
|
||||
|
||||
const document: TLDrawDocument = await TLDrawDocument.create(uri, openContext.backupId)
|
||||
this.disposables.push(
|
||||
document.onDidChange((e) => {
|
||||
nicelog('onDidChange')
|
||||
|
||||
// Tell VS Code that the document has been edited by the use.
|
||||
this._onDidChangeCustomDocument.fire({
|
||||
document,
|
||||
...e,
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
this.disposables.push(
|
||||
document.onDidChangeContent((e: DocumentChangeEventArgs) => {
|
||||
nicelog('onDidChange')
|
||||
|
||||
this.webviewPanels.forEach((w: vscode.WebviewPanel) => {
|
||||
if (w.active) {
|
||||
if (e.reason === 'undo' || e.reason === 'redo') {
|
||||
w.webview.postMessage({
|
||||
type: `vscode:${e.reason}`,
|
||||
} as VscodeMessage)
|
||||
} else if (e.reason === 'revert') {
|
||||
w.webview.postMessage({
|
||||
type: `vscode:revert`,
|
||||
data: {
|
||||
fileContents: JSON.stringify(e.fileContents),
|
||||
},
|
||||
} as VscodeMessage)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
document.onDidDispose(() => {
|
||||
nicelog('onDidDispose document in provider')
|
||||
this.disposables.forEach((d) => d.dispose())
|
||||
})
|
||||
|
||||
return document
|
||||
}
|
||||
|
||||
async resolveCustomEditor(
|
||||
document: TLDrawDocument,
|
||||
webviewPanel: vscode.WebviewPanel,
|
||||
_token: vscode.CancellationToken
|
||||
): Promise<void> {
|
||||
nicelog('resolveCustomEditor')
|
||||
this.webviewPanels.push(webviewPanel)
|
||||
webviewPanel.onDidDispose(() => {
|
||||
this.webviewPanels = this.webviewPanels.filter((w) => w !== webviewPanel)
|
||||
})
|
||||
new TldrawWebviewManager(this.context, document, webviewPanel)
|
||||
}
|
||||
|
||||
public saveCustomDocument(
|
||||
document: TLDrawDocument,
|
||||
cancellation: vscode.CancellationToken
|
||||
): Thenable<void> {
|
||||
return document.save(cancellation)
|
||||
}
|
||||
|
||||
public saveCustomDocumentAs(
|
||||
document: TLDrawDocument,
|
||||
destination: vscode.Uri,
|
||||
cancellation: vscode.CancellationToken
|
||||
): Thenable<void> {
|
||||
return document.saveAs(destination, cancellation)
|
||||
}
|
||||
|
||||
public revertCustomDocument(
|
||||
document: TLDrawDocument,
|
||||
cancellation: vscode.CancellationToken
|
||||
): Thenable<void> {
|
||||
return document.revert(cancellation)
|
||||
}
|
||||
|
||||
public backupCustomDocument(
|
||||
document: TLDrawDocument,
|
||||
context: vscode.CustomDocumentBackupContext,
|
||||
cancellation: vscode.CancellationToken
|
||||
): Thenable<vscode.CustomDocumentBackup> {
|
||||
return document.backup(context.destination, cancellation)
|
||||
}
|
||||
}
|
||||
82
apps/vscode/extension/src/TldrawOfflineFileProvider.ts
Normal file
82
apps/vscode/extension/src/TldrawOfflineFileProvider.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import * as vscode from 'vscode'
|
||||
|
||||
/**
|
||||
* tldraw offline saves documents as `.tldraw` archives — a zipped SQLite database rather than the
|
||||
* JSON a `.tldr` file holds — so nothing in this extension can read one. We claim the extension
|
||||
* anyway, and open a read-only panel explaining that, because the alternative is VS Code's generic
|
||||
* "file is not displayed because it is binary" placeholder, which says nothing about tldraw.
|
||||
*
|
||||
* This deliberately doesn't reuse `TldrawEditorProvider`: that path reads the file as UTF-8 and
|
||||
* hands it to `loadFile`, which would turn the archive's bytes into a corrupt-file error on a
|
||||
* mounted canvas.
|
||||
*/
|
||||
export class TldrawOfflineFileProvider implements vscode.CustomReadonlyEditorProvider {
|
||||
private static readonly viewType = 'tldraw.tldraw-offline'
|
||||
|
||||
public static register(): vscode.Disposable {
|
||||
return vscode.window.registerCustomEditorProvider(
|
||||
TldrawOfflineFileProvider.viewType,
|
||||
new TldrawOfflineFileProvider()
|
||||
)
|
||||
}
|
||||
|
||||
openCustomDocument(uri: vscode.Uri): vscode.CustomDocument {
|
||||
return { uri, dispose() {} }
|
||||
}
|
||||
|
||||
resolveCustomEditor(_document: vscode.CustomDocument, webviewPanel: vscode.WebviewPanel): void {
|
||||
webviewPanel.webview.html = html
|
||||
}
|
||||
}
|
||||
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>tldraw</title>
|
||||
<style>
|
||||
body {
|
||||
background-color: var(--vscode-editor-background);
|
||||
color: var(--vscode-editor-foreground);
|
||||
font-family: var(--vscode-font-family);
|
||||
font-size: var(--vscode-font-size);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
}
|
||||
.message {
|
||||
max-width: 24rem;
|
||||
text-align: center;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.message h1 {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 0.5rem;
|
||||
}
|
||||
.message p {
|
||||
margin: 0;
|
||||
opacity: 0.8;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="message">
|
||||
<h1>Can’t open .tldraw files yet</h1>
|
||||
<p>
|
||||
We’re working on support for files from
|
||||
<a href="https://offline.tldraw.com/">tldraw offline</a>.
|
||||
</p>
|
||||
<p>
|
||||
For now, you can
|
||||
<a href="https://tldraw.notion.site/User-manual-tldraw-offline-39a3e4c324c080e7b2eacc5afd078e85#3aa3e4c324c080669967e2cc3ae2c789">export as a .tldr file</a>
|
||||
to use it here.
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
82
apps/vscode/extension/src/TldrawWebviewManager.ts
Normal file
82
apps/vscode/extension/src/TldrawWebviewManager.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { uniqueId } from 'tldraw'
|
||||
import * as vscode from 'vscode'
|
||||
import { TLDrawDocument } from './TldrawDocument'
|
||||
import { GlobalStateKeys, WebViewMessageHandler } from './WebViewMessageHandler'
|
||||
|
||||
export class TldrawWebviewManager {
|
||||
private disposables: vscode.Disposable[] = []
|
||||
private webViewMessageHandler: WebViewMessageHandler
|
||||
|
||||
constructor(
|
||||
context: vscode.ExtensionContext,
|
||||
document: TLDrawDocument,
|
||||
webviewPanel: vscode.WebviewPanel
|
||||
) {
|
||||
let userId = context.globalState.get(GlobalStateKeys.UserId)
|
||||
if (!userId) {
|
||||
userId = 'user:' + uniqueId()
|
||||
context.globalState.update(GlobalStateKeys.UserId, userId)
|
||||
}
|
||||
|
||||
const assetSrc = webviewPanel.webview
|
||||
.asWebviewUri(vscode.Uri.joinPath(context.extensionUri, 'editor', '/'))
|
||||
.toString()
|
||||
|
||||
this.webViewMessageHandler = new WebViewMessageHandler(
|
||||
document,
|
||||
webviewPanel,
|
||||
context,
|
||||
userId,
|
||||
assetSrc
|
||||
)
|
||||
// Listen for messages sent from the extensions webview.
|
||||
webviewPanel.webview.onDidReceiveMessage(
|
||||
this.webViewMessageHandler.handle,
|
||||
undefined,
|
||||
this.disposables
|
||||
)
|
||||
|
||||
// Configure the webview. For now all we do is enable scripts and also
|
||||
// provide the initial webview's html content.
|
||||
Object.assign(webviewPanel.webview, {
|
||||
options: { enableScripts: true },
|
||||
html: this.getHtmlForWebview(assetSrc),
|
||||
})
|
||||
|
||||
const showV1FileOpenWarning = context.globalState.get(GlobalStateKeys.ShowV1FileOpenWarning)
|
||||
if (showV1FileOpenWarning === undefined) {
|
||||
context.globalState.update(GlobalStateKeys.ShowV1FileOpenWarning, true)
|
||||
}
|
||||
|
||||
// Clean up disposables when the editor is closed.
|
||||
webviewPanel.onDidDispose(this.handleDidDispose)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line tldraw/prefer-class-methods
|
||||
private handleDidDispose = () => {
|
||||
this.disposables.forEach(({ dispose }) => dispose())
|
||||
}
|
||||
|
||||
getHtmlForWebview(assetSrc: string) {
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="stylesheet" href="${assetSrc}/index.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>tldraw</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<noscript>You need to enable JavaScript to run this editor.</noscript>
|
||||
<script>
|
||||
// Plenty of other extensions do this see <https://sourcegraph.com/search?q=context%3Aglobal+%22_defaultStyles%22&patternType=standard&sm=1&groupBy=repo>
|
||||
document.getElementById("_defaultStyles").remove();
|
||||
</script>
|
||||
<script src="${assetSrc}/index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
}
|
||||
}
|
||||
221
apps/vscode/extension/src/WebViewMessageHandler.ts
Normal file
221
apps/vscode/extension/src/WebViewMessageHandler.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
import isEqual from 'lodash/isEqual'
|
||||
import { UnknownRecord } from 'tldraw'
|
||||
import * as vscode from 'vscode'
|
||||
// @ts-ignore
|
||||
import type { VscodeMessage } from '../../messages'
|
||||
import { loadFile } from './file'
|
||||
import { getMimeTypeFromPath } from './media'
|
||||
import { TLDrawDocument } from './TldrawDocument'
|
||||
import { unfurl } from './unfurl'
|
||||
import { nicelog } from './utils'
|
||||
|
||||
export const GlobalStateKeys = {
|
||||
ShowV1FileOpenWarning: 'showV1fileOpenWarning',
|
||||
UserId: 'userId',
|
||||
}
|
||||
|
||||
export class WebViewMessageHandler {
|
||||
multiplayerOmitKeys = /^(user_presence:|camera:|user:|user_document:|instance:)/
|
||||
newDocumentOmitKeys = /^(user_presence:|camera:|user:|user_document:|instance:|document:|page:)/
|
||||
|
||||
constructor(
|
||||
private document: TLDrawDocument,
|
||||
private webviewPanel: vscode.WebviewPanel,
|
||||
private context: vscode.ExtensionContext,
|
||||
private userId: unknown,
|
||||
private assetSrc: string
|
||||
) {}
|
||||
|
||||
isLoaded = false
|
||||
|
||||
// eslint-disable-next-line tldraw/prefer-class-methods
|
||||
handle = async (e: VscodeMessage) => {
|
||||
if (!this.document) return
|
||||
|
||||
switch (e.type) {
|
||||
case 'vscode:ready-to-receive-file': {
|
||||
// Send the initial document content to bootstrap the Tldraw/Tldraw component.
|
||||
this.webviewPanel.webview.postMessage({
|
||||
type: 'vscode:opened-file',
|
||||
data: {
|
||||
fileContents: JSON.stringify(this.document.documentData),
|
||||
uri: this.document.uri.toString(),
|
||||
userId: this.userId,
|
||||
assetSrc: this.assetSrc,
|
||||
isDarkMode:
|
||||
this.document.isBlankDocument &&
|
||||
(vscode.window.activeColorTheme.kind === 2 ||
|
||||
vscode.window.activeColorTheme.kind === 3),
|
||||
},
|
||||
} as VscodeMessage)
|
||||
break
|
||||
}
|
||||
case 'vscode:open-window': {
|
||||
vscode.env.openExternal(vscode.Uri.parse(e.data.url))
|
||||
break
|
||||
}
|
||||
case 'vscode:undo': {
|
||||
vscode.commands.executeCommand('undo')
|
||||
break
|
||||
}
|
||||
case 'vscode:redo': {
|
||||
vscode.commands.executeCommand('redo')
|
||||
break
|
||||
}
|
||||
case 'vscode:refresh-page': {
|
||||
vscode.commands.executeCommand('workbench.action.reloadWindow')
|
||||
break
|
||||
}
|
||||
case 'vscode:hard-reset': {
|
||||
await this.document.loadBlankDocument()
|
||||
vscode.commands.executeCommand('workbench.action.reloadWindow')
|
||||
break
|
||||
}
|
||||
case 'vscode:get-file/request': {
|
||||
try {
|
||||
const url = e.data.url
|
||||
const fileUri = vscode.Uri.parse(url)
|
||||
const fileData = await vscode.workspace.fs.readFile(fileUri)
|
||||
const mimeType = getMimeTypeFromPath(fileUri.path)
|
||||
|
||||
this.webviewPanel.webview.postMessage({
|
||||
type: 'vscode:get-file/response',
|
||||
uuid: e.uuid + '_response',
|
||||
data: {
|
||||
fileName: fileUri.path.split('/').pop(),
|
||||
file: Array.from(fileData),
|
||||
mimeType,
|
||||
},
|
||||
})
|
||||
} catch (error: any) {
|
||||
this.webviewPanel.webview.postMessage({
|
||||
type: 'vscode:get-file/error',
|
||||
data: {
|
||||
error: error.toString(),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
case 'vscode:bookmark/request': {
|
||||
const url = e.data.url
|
||||
await unfurl(url)
|
||||
.then(async (json: any) => {
|
||||
this.webviewPanel.webview.postMessage({
|
||||
type: 'vscode:bookmark/response',
|
||||
// Add a suffix to the uuid to represent the response.
|
||||
uuid: e.uuid + '_response',
|
||||
data: {
|
||||
url,
|
||||
title: json.title,
|
||||
description: json.description,
|
||||
image: json.image,
|
||||
favicon: json.favicon,
|
||||
},
|
||||
})
|
||||
})
|
||||
.catch((error: any) => {
|
||||
this.webviewPanel.webview.postMessage({
|
||||
type: 'vscode:bookmark/error',
|
||||
data: {
|
||||
error: error.toString(),
|
||||
},
|
||||
})
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'vscode:editor-loaded': {
|
||||
this.isLoaded = true
|
||||
break
|
||||
}
|
||||
case 'vscode:v1-file-opened': {
|
||||
const showV1FileOpenWarning = this.context.globalState.get(
|
||||
GlobalStateKeys.ShowV1FileOpenWarning
|
||||
)
|
||||
|
||||
if (!showV1FileOpenWarning) return
|
||||
|
||||
const { backup, open, description, dontAskAgain } = e.data
|
||||
vscode.window
|
||||
.showInformationMessage(description, open, dontAskAgain, backup)
|
||||
.then((result) => {
|
||||
if (result === backup) {
|
||||
this.document.v1Backup(e.data.backupSaved, e.data.backupFailed)
|
||||
} else if (result === dontAskAgain) {
|
||||
this.context.globalState.update(GlobalStateKeys.ShowV1FileOpenWarning, false)
|
||||
}
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'vscode:editor-updated': {
|
||||
if (!this.isLoaded) return
|
||||
|
||||
const raw = e.data.fileContents
|
||||
if (!raw) return
|
||||
|
||||
// The event will contain the new TDFile as JSON.
|
||||
const nextFile = loadFile(raw)
|
||||
const existingDoc = this.document.documentData
|
||||
|
||||
let isSame = false
|
||||
|
||||
if (existingDoc?.records?.length > 0) {
|
||||
const oldDoc = this.omit(existingDoc.records, this.multiplayerOmitKeys)
|
||||
const newDoc = this.omit(nextFile.records, this.multiplayerOmitKeys)
|
||||
isSame = isEqual(oldDoc, newDoc)
|
||||
} else {
|
||||
const newDoc = this.omit(nextFile.records, this.newDocumentOmitKeys)
|
||||
isSame = isEqual(newDoc, [])
|
||||
}
|
||||
|
||||
if (!isSame) {
|
||||
this.document.makeEdit(nextFile)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'vscode:hide-v1-file-open-warning': {
|
||||
this.context.globalState.update(GlobalStateKeys.ShowV1FileOpenWarning, false)
|
||||
break
|
||||
}
|
||||
case 'vscode:cancel-v1-migrate': {
|
||||
vscode.commands.executeCommand('workbench.action.closeActiveEditor')
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
omit(records: UnknownRecord[], keys: RegExp) {
|
||||
return records.filter((record) => {
|
||||
return !record.id.match(keys)
|
||||
})
|
||||
}
|
||||
|
||||
findDiff(oldDoc: Record<string, any>, newDoc: Record<string, any>) {
|
||||
const newRecords = Object.values(newDoc)
|
||||
const oldRecords = Object.values(oldDoc)
|
||||
|
||||
for (const oldRecord of oldRecords) {
|
||||
const newRecord = newRecords.find((r: any) => r.id === oldRecord.id)
|
||||
if (!newRecord) {
|
||||
nicelog('record missing in new doc', oldRecord)
|
||||
continue
|
||||
} else {
|
||||
if (!isEqual(oldRecord, newRecord)) {
|
||||
nicelog('record different', oldRecord, newRecord)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const newRecord of newRecords) {
|
||||
const oldRecord = oldRecords.find((r: any) => r.id === newRecord.id)
|
||||
if (!oldRecord) {
|
||||
nicelog('record missing in oldDoc doc', newRecord)
|
||||
continue
|
||||
} else {
|
||||
if (!isEqual(newRecord, oldRecord)) {
|
||||
nicelog('record different', newRecord, oldRecord)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
38
apps/vscode/extension/src/extension.ts
Normal file
38
apps/vscode/extension/src/extension.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { watch } from 'fs'
|
||||
import path from 'path'
|
||||
import * as vscode from 'vscode'
|
||||
import { TldrawEditorProvider } from './TldrawEditorProvider'
|
||||
import { TldrawOfflineFileProvider } from './TldrawOfflineFileProvider'
|
||||
import { nicelog } from './utils'
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
try {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
const extensionWatcher = watch(
|
||||
__dirname + '/extension.js',
|
||||
{ persistent: false },
|
||||
(eventType, filename) => {
|
||||
nicelog('reloading[%s]', eventType, filename)
|
||||
extensionWatcher.close()
|
||||
vscode.commands.executeCommand('workbench.action.reloadWindow')
|
||||
}
|
||||
)
|
||||
const dirname = path.dirname(__dirname)
|
||||
const editorpath = dirname.slice(0, dirname.lastIndexOf(path.sep))
|
||||
const editorWatcher = watch(
|
||||
editorpath + '/editor/index.js',
|
||||
{ persistent: false },
|
||||
(eventType, filename) => {
|
||||
nicelog('reloading[%s]', eventType, filename)
|
||||
editorWatcher.close()
|
||||
vscode.commands.executeCommand('workbench.action.reloadWindow')
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
context.subscriptions.push(TldrawEditorProvider.register(context))
|
||||
context.subscriptions.push(TldrawOfflineFileProvider.register())
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
36
apps/vscode/extension/src/file.ts
Normal file
36
apps/vscode/extension/src/file.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { TldrawFile, createTLSchema } from 'tldraw'
|
||||
import * as vscode from 'vscode'
|
||||
import { nicelog } from './utils'
|
||||
|
||||
export const defaultFileContents: TldrawFile = {
|
||||
tldrawFileFormatVersion: 1,
|
||||
schema: createTLSchema().serialize(),
|
||||
records: [],
|
||||
}
|
||||
|
||||
export const fileContentWithErrors: TldrawFile = {
|
||||
tldrawFileFormatVersion: 1,
|
||||
schema: createTLSchema().serialize(),
|
||||
records: [{ typeName: 'shape', id: null } as any],
|
||||
}
|
||||
|
||||
export function loadFile(fileContents: string): TldrawFile {
|
||||
if (!fileContents) return defaultFileContents
|
||||
try {
|
||||
return JSON.parse(fileContents) as TldrawFile
|
||||
} catch {
|
||||
return fileContentWithErrors
|
||||
}
|
||||
}
|
||||
|
||||
export async function fileExists(destination: vscode.Uri) {
|
||||
try {
|
||||
await vscode.workspace.fs.stat(destination)
|
||||
return true
|
||||
} catch (e: any) {
|
||||
if (e.code !== 'FileNotFound') {
|
||||
nicelog(e)
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
50
apps/vscode/extension/src/media.ts
Normal file
50
apps/vscode/extension/src/media.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { DEFAULT_SUPPORTED_MEDIA_TYPES } from 'tldraw'
|
||||
|
||||
const EXTENSION_TO_DEFAULT_MIME_TYPE = {
|
||||
// Vector image extensions
|
||||
svg: 'image/svg+xml',
|
||||
|
||||
// Static image extensions
|
||||
jpeg: 'image/jpeg',
|
||||
png: 'image/png',
|
||||
webp: 'image/webp',
|
||||
|
||||
// Animated image extensions
|
||||
gif: 'image/gif',
|
||||
apng: 'image/apng',
|
||||
avif: 'image/avif',
|
||||
|
||||
// Video extensions
|
||||
mp4: 'video/mp4',
|
||||
webm: 'video/webm',
|
||||
mov: 'video/quicktime',
|
||||
qt: 'video/quicktime',
|
||||
} as const
|
||||
|
||||
type DefaultSupportedMimeType = (typeof DEFAULT_SUPPORTED_MEDIA_TYPES)[number]
|
||||
type DefaultSupportedExtensions = keyof typeof EXTENSION_TO_DEFAULT_MIME_TYPE
|
||||
|
||||
// We use these types to enforce that we don't have any extra mime types in the EXTENSION_TO_MIME_TYPE map
|
||||
// and that we don't miss any mime types in the DEFAULT_SUPPORTED_MEDIA_TYPES array
|
||||
type ExtensionMapMimeTypes =
|
||||
(typeof EXTENSION_TO_DEFAULT_MIME_TYPE)[keyof typeof EXTENSION_TO_DEFAULT_MIME_TYPE]
|
||||
type _CheckMimeTypesCovered = DefaultSupportedMimeType extends ExtensionMapMimeTypes ? true : never
|
||||
type _CheckNoExtraMimeTypes = ExtensionMapMimeTypes extends DefaultSupportedMimeType ? true : never
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const mimeTypesCovered: _CheckMimeTypesCovered = true
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const noExtraMimeTypes: _CheckNoExtraMimeTypes = true
|
||||
|
||||
function isSupportedExtension(extension: string): extension is DefaultSupportedExtensions {
|
||||
return extension in EXTENSION_TO_DEFAULT_MIME_TYPE
|
||||
}
|
||||
|
||||
export function getMimeTypeFromPath(filePath: string): string {
|
||||
const extension = filePath.split('.').pop()?.toLowerCase()
|
||||
if (!extension) throw new Error('No extension found in file path')
|
||||
|
||||
if (isSupportedExtension(extension)) {
|
||||
return EXTENSION_TO_DEFAULT_MIME_TYPE[extension]
|
||||
}
|
||||
throw new Error(`Unsupported file type: ${extension}`)
|
||||
}
|
||||
69
apps/vscode/extension/src/unfurl.ts
Normal file
69
apps/vscode/extension/src/unfurl.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { load } from 'cheerio'
|
||||
|
||||
export async function unfurl(url: string) {
|
||||
// Let's see if this URL was an image to begin with.
|
||||
if (url.match(/\.(a?png|jpe?g|gif|svg|webp|avif)$/i)) {
|
||||
return {
|
||||
title: undefined,
|
||||
description: undefined,
|
||||
image: url,
|
||||
favicon: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(url)
|
||||
if (response.status >= 400) {
|
||||
throw new Error(`Error fetching url: ${response.status}`)
|
||||
}
|
||||
const contentType = response.headers.get('content-type')
|
||||
if (!contentType?.includes('text/html')) {
|
||||
throw new Error(`Content-type not right: ${contentType}`)
|
||||
}
|
||||
if (contentType?.startsWith('image/')) {
|
||||
return {
|
||||
title: undefined,
|
||||
description: undefined,
|
||||
image: url,
|
||||
favicon: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
const content = await response.text()
|
||||
const $ = load(content)
|
||||
|
||||
const og: { [key: string]: string | undefined } = {}
|
||||
const twitter: { [key: string]: string | undefined } = {}
|
||||
|
||||
$('meta[property^=og:]').each((_, el) => {
|
||||
og[$(el).attr('property')!] = $(el).attr('content')
|
||||
})
|
||||
$('meta[name^=twitter:]').each((_, el) => {
|
||||
twitter[$(el).attr('name')!] = $(el).attr('content')
|
||||
})
|
||||
|
||||
const title = og['og:title'] ?? twitter['twitter:title'] ?? $('title').text() ?? undefined
|
||||
const description =
|
||||
og['og:description'] ??
|
||||
twitter['twitter:description'] ??
|
||||
$('meta[name="description"]').attr('content') ??
|
||||
undefined
|
||||
let image = og['og:image:secure_url'] ?? og['og:image'] ?? twitter['twitter:image'] ?? undefined
|
||||
let favicon =
|
||||
$('link[rel="apple-touch-icon"]').attr('href') ??
|
||||
$('link[rel="icon"]').attr('href') ??
|
||||
undefined
|
||||
|
||||
if (image && !image?.startsWith('http')) {
|
||||
image = new URL(image, url).href
|
||||
}
|
||||
if (favicon && !favicon?.startsWith('http')) {
|
||||
favicon = new URL(favicon, url).href
|
||||
}
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
image,
|
||||
favicon,
|
||||
}
|
||||
}
|
||||
8
apps/vscode/extension/src/utils.ts
Normal file
8
apps/vscode/extension/src/utils.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
const DEBUG_EVENTS = false
|
||||
|
||||
export function nicelog(...args: any[]) {
|
||||
if (process.env.NODE_ENV !== 'production' && DEBUG_EVENTS) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(...args)
|
||||
}
|
||||
}
|
||||
25
apps/vscode/extension/tsconfig.json
Normal file
25
apps/vscode/extension/tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"removeComments": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"composite": true,
|
||||
"importHelpers": false,
|
||||
"skipDefaultLibCheck": true
|
||||
},
|
||||
"include": ["src", "../messages", "scripts"],
|
||||
"references": [{ "path": "../../../packages/editor" }, { "path": "../../../packages/tldraw" }]
|
||||
}
|
||||
Reference in New Issue
Block a user