Create VS Code Extension (#4)

* Start of vscode extension. Current code is copy/paste from custom editor samples from Microsoft. We need to evaluate if using their text based customer editor or full on new custom editor is the way to go

* Not sure how I missed these files. Adding them

* Have a custom editor triggering off of .tldr files. Added gitignores for generated folder. Have iframed tldraw loading and security policies set to do so

* Can now load a .tldr file. No saving support yet. Load times are slow, mostly from editor loading up I think

* Have temporary solution for saving working now too.

* Missed af ile

* Backing up progress in syncing tldraw editor history changes

* Removed console

* ...

* ...

* Cleanup

* Have save working well now.

* Moved extension into 'integrations' folder

* Trying out WebviewPanelOptions.retainContextWhenHidden=true and it's looking promising

* Some cleanup

* Trying out new  @tldraw/editor module

* Have prototype loading using new embedded editor

* ...

* Shaved off 1 second from editor loadtime

* Got save working again. Had to manually fixuppreviously created .tldr files as the format changed a bit

* More tuning

* Starting work to get new tldraw/tldraw working.

* Added example tldr files to vscode package

* Removed old editor package

* Have onChange working with latest fix. Back to iframed for a few mom

* Fixed up .tldr files

* Have iframe free extension working, but requiring hand crafted building

* ...

* Better handling of empty .tldr files. Still an issue with freshly created files trying to save as .js or .json

* Thoroughly added comments for the extension code. Need to add diagrams though and now will document/comment/diagram the editor src code

* Added comments to all of the editor side of the VS Code Extension. Also cleaned up the code

* More cleanup of VS Code Extension code and have script automating generating the initial webview's html content from the cra editor static build

* Tweaks to watch logic

* Improved scripts for publishing to VS Code Marketplace

* Improved name

* Made the smiley angry

* Reverted

* Turned smiley mad

* Turned smiley mad

* Made smiley sad

* Have a lot of plumbing working for Github codespaces and github.dev support

* Imported new tldraw vs code extension code. Added instructions for workflows

* Quick fix

* Fix for corrupted arrows files

* Updated editor build step to new location

* Merge branch 'main' into vscode-extension-v1, add local file updating

* Update App.tsx

* Cleanup, bumped to 0.0.124 @tldraw/tdlraw and published a 0.10.0 version of hte extension

* Added Trello/Kanban style file

* Finished video

* brings up to date

* Fix scripts

* Update README.md

* Update .babelrc

Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
This commit is contained in:
Francois Laberge 2021-11-06 12:49:53 -04:00 committed by GitHub
parent 84bd8345e3
commit 0b15992464
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 9577 additions and 11 deletions

7
.eslintignore Normal file
View file

@ -0,0 +1,7 @@
**/node_modules/*
**/out/*
**/.next/*
# TODO: Remove these, vscode experiments are based on existing examples and are punting on making eslint happy just yet.
**/integrations/vscode-extension/*
**/integrations/vscode-simple/*

View file

@ -10,15 +10,5 @@
<div id="root"></div> <div id="root"></div>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<script type="module" src="./index.js"></script> <script type="module" src="./index.js"></script>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body> </body>
</html> </html>

View file

@ -9,6 +9,7 @@
}, },
"license": "MIT", "license": "MIT",
"workspaces": [ "workspaces": [
"vscode/editor",
"packages/tldraw", "packages/tldraw",
"example", "example",
"www" "www"
@ -17,6 +18,7 @@
"test": "jest", "test": "jest",
"test:watch": "jest --watchAll", "test:watch": "jest --watchAll",
"lerna": "lerna", "lerna": "lerna",
"start:vscode": "lerna run start:vscode --stream --parallel",
"start": "lerna run start --stream --parallel", "start": "lerna run start --stream --parallel",
"start:www": "yarn build:packages && lerna run start --parallel & cd www && yarn dev", "start:www": "yarn build:packages && lerna run start --parallel & cd www && yarn dev",
"build": "yarn build:packages && cd www && yarn build", "build": "yarn build:packages && cd www && yarn build",

View file

@ -22,6 +22,7 @@
"module": "./dist/esm/index.js", "module": "./dist/esm/index.js",
"types": "./dist/types/index.d.ts", "types": "./dist/types/index.d.ts",
"scripts": { "scripts": {
"start:vscode": "yarn start",
"start": "node scripts/dev & yarn types:dev", "start": "node scripts/dev & yarn types:dev",
"build": "node scripts/build && yarn types:build && node scripts/copy-readme", "build": "node scripts/build && yarn types:build && node scripts/copy-readme",
"types:dev": "tsc -w", "types:dev": "tsc -w",

View file

@ -314,9 +314,12 @@ function InnerTldraw({
React.useEffect(() => { React.useEffect(() => {
if (!document) return if (!document) return
if (document.id === tlstate.document.id) { if (document.id === tlstate.document.id) {
console.log('updating')
tlstate.updateDocument(document) tlstate.updateDocument(document)
} else { } else {
console.log('loading')
tlstate.loadDocument(document) tlstate.loadDocument(document)
} }
}, [document, tlstate]) }, [document, tlstate])

View file

@ -780,6 +780,8 @@ export class TLDrawState extends StateManager<Data> {
} }
} }
nextState.document = migrate(nextState.document, nextState.document.version || 0)
return this.replaceState(nextState, `${reason}:${document.id}`) return this.replaceState(nextState, `${reason}:${document.id}`)
} }

15
vscode/README.md Normal file
View file

@ -0,0 +1,15 @@
# @tldraw/vscode
This folder contains the source for the tldraw VSCode extension.
## 1. Setup editor project
- Open `vscode/editor`
- Install dependencies (`yarn`)
- Start the development server (`yarn start`)
## 2. Start the Extension
- Open `vscode/extension` in a new VSCode window
- Install dependencies (`yarn`)
- Run the extension (F5)

21
vscode/editor/LICENSE.md Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Stephen Ruiz Ltd
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.

6
vscode/editor/README.md Normal file
View file

@ -0,0 +1,6 @@
# @tldraw/tldraw-example
An example for @tldraw/tldraw with a very fast dev server.
**Note:** You probably do not need to start the server here: it is started as
part of `yarn start` in the root directory.

View file

@ -0,0 +1,41 @@
/* eslint-disable no-undef */
import fs from 'fs'
import esbuild from 'esbuild'
import serve, { error, log } from 'create-serve'
if (!fs.existsSync('./dist')) {
fs.mkdirSync('./dist')
}
fs.copyFile('./src/index.html', './dist/index.html', (err) => {
if (err) throw err
})
esbuild
.build({
entryPoints: ['src/index.tsx'],
outfile: 'dist/index.js',
minify: false,
bundle: true,
sourcemap: true,
incremental: true,
format: 'esm',
target: 'esnext',
define: {
'process.env.LIVEBLOCKS_PUBLIC_API_KEY': process.env.LIVEBLOCKS_PUBLIC_API_KEY,
'process.env.NODE_ENV': '"development"',
},
watch: {
onRebuild(err) {
serve.update()
err ? error('❌ Failed') : log('✅ Updated')
},
},
})
.catch(() => process.exit(1))
serve.start({
port: 5000,
root: './dist',
live: true,
})

View file

@ -0,0 +1,34 @@
{
"name": "@tldraw/vscode-editor",
"version": "0.1.4",
"private": true,
"description": "An an editor for the tldraw vscode extension.",
"author": "@steveruizok",
"license": "MIT",
"keywords": [
"react",
"typescript",
"esbuild"
],
"scripts": {
"start:vscode": "node scripts/dev.mjs -w",
"start": "node scripts/dev.mjs -w",
"build": "node scripts/build.mjs"
},
"devDependencies": {
"@tldraw/tldraw": "^0.1.4",
"@types/node": "^14.14.35",
"@types/react": "^16.9.55",
"@types/react-dom": "^16.9.9",
"@types/react-router-dom": "^5.1.8",
"concurrently": "6.0.1",
"create-serve": "1.0.1",
"esbuild": "^0.13.8",
"esbuild-serve": "^1.0.1",
"react": ">=16.8",
"react-dom": "^16.8 || ^17.0",
"rimraf": "3.0.2",
"typescript": "4.2.3"
},
"gitHead": "a7dac0f83ad998e205c2aab58182cb4ba4e099a6"
}

View file

@ -0,0 +1,40 @@
/* eslint-disable */
import fs from 'fs'
import esbuild from 'esbuild'
import { createRequire } from 'module'
const pkg = createRequire(import.meta.url)('../package.json')
async function main() {
if (fs.existsSync('./dist')) {
fs.rmSync('./dist', { recursive: true }, (e) => {
if (e) {
throw e
}
})
}
try {
esbuild.buildSync({
entryPoints: ['./src/index.tsx'],
outfile: 'dist/index.js',
minify: false,
bundle: true,
format: 'cjs',
target: 'es6',
jsxFactory: 'React.createElement',
jsxFragment: 'React.Fragment',
tsconfig: './tsconfig.json',
define: {
'process.env.NODE_ENV': '"production"',
},
})
console.log(`${pkg.name}: Build completed.`)
} catch (e) {
console.log(`× ${pkg.name}: Build failed due to an error.`)
console.log(e)
}
}
main()

View file

@ -0,0 +1,40 @@
/* eslint-disable no-undef */
import fs from 'fs'
import esbuildServe from 'esbuild-serve'
async function main() {
if (!fs.existsSync('./dist')) {
fs.mkdirSync('./dist')
}
try {
await esbuildServe(
{
entryPoints: ['src/index.tsx'],
outfile: 'dist/index.js',
minify: false,
bundle: true,
incremental: true,
target: 'es6',
define: {
'process.env.NODE_ENV': '"development"',
},
watch: {
onRebuild(err) {
serve.update()
err ? error('❌ Failed') : log('✅ Updated')
},
},
},
{
port: 5420,
root: './dist',
live: true,
}
)
} catch (err) {
process.exit(1)
}
}
main()

38
vscode/editor/src/app.tsx Normal file
View file

@ -0,0 +1,38 @@
import * as React from 'react'
import { TLDraw, TLDrawState, TLDrawDocument, Data, TLDrawFile } from '@tldraw/tldraw'
import { vscode } from './utils/vscode'
import { eventsRegex } from './utils/eventsRegex'
import { defaultDocument } from './utils/defaultDocument'
import { UI_EVENT } from './types'
import './styles.css'
// Will be placed in global scope by extension
declare let currentFile: TLDrawFile
export default function App(): JSX.Element {
const rTLDrawState = React.useRef<TLDrawState>()
// When the editor mounts, save the tlstate instance in a ref.
const handleMount = React.useCallback((tldr: TLDrawState) => {
rTLDrawState.current = tldr
}, [])
// When the editor's document changes, post the stringified document to the vscode extension.
const handleChange = React.useCallback((tldr: TLDrawState, data: Data, type: string) => {
if (type.search(eventsRegex) === 0) {
vscode.postMessage({ type: UI_EVENT.TLDRAW_UPDATED, text: JSON.stringify(tldr.document) })
}
}, [])
return (
<div className="tldraw">
<TLDraw
id={currentFile.document.id}
document={currentFile.document ?? defaultDocument}
onMount={handleMount}
onChange={handleChange}
autofocus
/>
</div>
)
}

View file

@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom'
import App from './app'
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
)

View file

@ -0,0 +1,20 @@
html,
* {
box-sizing: border-box;
}
body {
overscroll-behavior: none;
margin: 0px;
padding: 0px;
}
.tldraw {
position: fixed;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
width: 100%;
height: 100%;
}

View file

@ -0,0 +1,7 @@
export enum UI_EVENT {
TLDRAW_UPDATED = 'TLDRAW_UPDATED',
}
export enum EXTENSION_EVENT {
LOCAL_FILE_UPDATED = 'LOCAL_FILE_UPDATED',
}

View file

@ -0,0 +1,24 @@
import type { TLDrawDocument } from '@tldraw/tldraw'
export const defaultDocument: TLDrawDocument = {
id: 'doc',
pages: {
page: {
id: 'page',
name: 'Page 1',
childIndex: 1,
shapes: {},
bindings: {},
},
},
pageStates: {
page: {
id: 'page',
selectedIds: [],
camera: {
point: [0, 0],
zoom: 1,
},
},
},
}

View file

@ -0,0 +1,4 @@
// A regex to detect event types that we should send to the extension. We only want to send
// events that change the document and that would be committed to the undo/redo stack; not
// all the smaller patches or transient changes from sessions.
export const eventsRegex = /command|undo|redo/

View file

@ -0,0 +1,8 @@
import type { UI_EVENT } from '../types'
// Will be placed in global scope by extension
declare function acquireVsCodeApi(): {
postMessage(options: { type: UI_EVENT; text?: string }): void
}
export const vscode = acquireVsCodeApi()

View file

@ -0,0 +1,23 @@
{
"extends": "../../tsconfig.base.json",
"include": ["src"],
"exclude": ["node_modules", "dist", "docs"],
"compilerOptions": {
"outDir": "./dist/types",
"rootDir": "src",
"baseUrl": "src",
"emitDeclarationOnly": false,
"paths": {
"@tldraw/tldraw": ["../../packages/tldraw"]
}
},
"references": [
{
"path": "../../packages/tldraw"
}
],
"typedocOptions": {
"entryPoints": ["src/index.ts"],
"out": "docs"
}
}

File diff suppressed because it is too large Load diff

1275
vscode/editor/yarn.lock Normal file

File diff suppressed because it is too large Load diff

6
vscode/extension/.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
dist
node_modules
.vscode-test-web/
*.vsix
editor-build
.DS_Store

View file

@ -0,0 +1,5 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": ["dbaeumer.vscode-eslint"]
}

38
vscode/extension/.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,38 @@
// 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 in VS Code",
"type": "pwa-extensionHost",
"debugWebWorkerHost": true,
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}",
"--extensionDevelopmentKind=web"
],
"outFiles": [
"${workspaceFolder}/dist/web/**/*.js"
],
"preLaunchTask": "npm: watch-web"
},
{
"name": "Extension Tests in VS Code",
"type": "extensionHost",
"debugWebWorkerHost": true,
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}",
"--extensionDevelopmentKind=web",
"--extensionTestsPath=${workspaceFolder}/dist/web/test/suite/index"
],
"outFiles": [
"${workspaceFolder}/dist/web/**/*.js"
],
"preLaunchTask": "npm: watch-web"
}
]
}

29
vscode/extension/.vscode/tasks.json vendored Normal file
View file

@ -0,0 +1,29 @@
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "compile-web",
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": [
"$ts-webpack",
"$tslint-webpack"
]
},
{
"type": "npm",
"script": "watch-web",
"group": "build",
"isBackground": true,
"problemMatcher": [
"$ts-webpack-watch",
"$tslint-webpack-watch"
]
}
]
}

View file

@ -0,0 +1,110 @@
### VS Code Extension
This folder contains code implementing a VS Code extension for working with tldraw within VS Code. Files are saved locally and thus play well with version control. Target use case is developer documentation.
### Todos
- Get live reloading working while extension developing (debug launched)
- Watching and rebuilding is working, it's just hot-updating the running editor instances that's failing
- It currently at least updates every time you open a new editor, currently it's the websocket
needed for the live reloading logic that is failing to connect.
- Backlog
- Make sure extension works in Codespaces (when one of us gets into the beta)
- [Supporting Remote Development and GitHub Codespaces](https://code.visualstudio.com/api/advanced-topics/remote-extensions)
## Test Cases
Here's a list of functionality and behavior we'd like working for an initial beta.
- Saving
- Save (Command+S or menu) should work
- Save As (menu or shortcut)
- Undo/Redo
- Should reflect correctly in VS Code menus (can/can't do it based on stack)
- Create new file using either of these methods
- Use VS Code's "New File", name the file with a .tldr extension
- Use the Tldraw extensions registered command to create a blank .tldr file
- Opening an
- The editor should communicate that a file isn't valid when loading it
- Detect and prevent multiple instances of the editor (For now)
Things we're cutting
- Distinguishing app state from user state (selection, zoom, pan, etc)
## Running The Extension
- Setup/Run the editor (a create react app that houses the tldraw component)
- `cd integrations/vscode/editor`
- `yarn`
- `yarn start`
- To setup/run the extension
- In a new terminal
- Install dependencies
- `yarn`
- Open just the extension folder in VS Code (necessary to use it's launch.json)
- `cd integrations/vscode/extension`
- `code .`
- Run the extension using F5 (the launch.json setups watching too)
- This will open a new VS Code window where the extension is installed in memory
- Open a folder containing some .tldr files. Ex. tldraw/integrations/extension/examples
- Select a .tldr file to test
- You'll have to toggle between the extension/editor/debug VS Code instances to change things and see the results
- NOTE: Hot reloading doesn't work right now for the editor's create-react-app workflow. I have yet to figure out how to set the websocket host, which currently assumes it's housed on the same host as the page it's loaded in (which for the extension is some weird custom protocol)
- If you close and reopen a .tldr file, it will load the latest change though
## Publishing/Packaging Extensions
**This stuff needs to be fully automated!!! We're not quite there yet.**
- Make sure you have the vsce command line tool installed
- `npm install -g vsce`
- Build the editor static site
- `cd integrations/vscode/editor`
- `yarn build`
- This will build a static version of the create react app and put it into `integrations/vscode/editor`.
- Bump the package.json version
- Just do this manually in the file
- Change the code in `integrations/vscode/extension/src/get-html.ts` so it uses the production path. It should look like this:
//return getDevModeHTML(context, webview, documentContent);
return getProductionModeHTML(context, webview, documentContent);
- Run script that copies over the latest static file path hashes
- **TODO**: Find this script. It seems to have gone missing and I can't find it in history
- The manual version is to look at the latest editor-build/index.html and copy/reference it into get-html.ts
- Compile the extension in production mode
- `npm run package-web`
- Package up the extension as an extension installer .vsix
- `vsce package`
- A file with a name like: `wardlt-0.8.0.vsix` will now be put in the extension root
- TODO: Make this get build to a proper temp directory like dist
- Before publishing test using the .vsix based installation workflow available in VS Code Desktop
- Use the Web UI to publish the latest extension by uploading the .vsix file
- https://marketplace.visualstudio.com/manage/publishers/Wardlt
- Click on the elipse button and choose Update, then select the .vsix we just generated
- It should take a few minutes to go through some automated validation the VS Code Marketplace does
- Now test it quickly
- Desktop
- github.dev
- Go here and then press '.' https://github.com/tldraw/tldraw/tree/vscode-extension-v1/integrations/vscode/extension/examples
- Try one of the .tldr files
- Codespaces
- Go here: https://github.com/conveyhq/codespaces-test
- Select Code -> main or go straight there via link: https://seflless-conveyhq-codespaces-test-x76jf9xq7.github.dev/
- I have a test code space here with a .tldr file already included
- Read more about this topic here: [Publishing Extensions](https://code.visualstudio.com/api/working-with-extensions/publishing-extension)
#### Good References
- [VS Code Marketplace Manager](https://marketplace.visualstudio.com/manage/)
- [Web Extensions Guide](https://code.visualstudio.com/api/extension-guides/web-extensions)
- [Test Your Web Extension](https://code.visualstudio.com/api/extension-guides/web-extensions#test-your-web-extension)
- [Web Extension Testing](https://code.visualstudio.com/api/extension-guides/web-extensions#web-extension-tests)
- An example custom editor that does work as a Web Extension
- https://marketplace.visualstudio.com/items?itemName=hediet.vscode-drawio
- https://github.com/hediet/vscode-drawio
- [VS Code Extension API/Landing Page](https://code.visualstudio.com/api)
- [Getting Started](https://code.visualstudio.com/api/get-started/your-first-extension)
- [Custom Editor API](https://code.visualstudio.com/api/extension-guides/custom-editors)
- [github.com/microsoft/vscode-extension-samples](https://github.com/microsoft/vscode-extension-samples)
- [Extensions Guide -> Webviews](https://code.visualstudio.com/api/extension-guides/webview)
- [Publishing Extensions](https://code.visualstudio.com/api/working-with-extensions/publishing-extension)

View file

@ -0,0 +1,13 @@
# Wardlt
This is an awesome sketch/diagramming extension for making doodles and diagrams of system. It allows you to naturally create informal but legible diagrams, designed to be version controlled along side your code.
## Features
1. Hand drawn style
2. Built in shapes like rectangles and ovals
3. Add text elements
4. Has smart arrows that support smart connecting between shapes/text for easy to make flow charts.
5. Supports dashed strokes on all shapes.
## Usage
Create a new file by either creating a blank .tldr file or by using the provided command: "Tldraw: Create a new .tldr file".

View file

@ -0,0 +1,124 @@
{
"fileHandle": null,
"document": {
"id": "doc",
"version": 13,
"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": [
351.95,
77.76
],
"size": [
174.79,
98.23
],
"rotation": 0,
"style": {
"color": "black",
"size": "small",
"isFilled": false,
"dash": "draw"
}
},
"fe792a2f-5a42-46dd-260e-a4135536d16d": {
"id": "fe792a2f-5a42-46dd-260e-a4135536d16d",
"type": "rectangle",
"name": "Rectangle",
"parentId": "page",
"childIndex": 2,
"point": [
137.22,
202.71
],
"size": [
249.64,
180.38
],
"rotation": 0,
"style": {
"color": "black",
"size": "small",
"isFilled": false,
"dash": "draw"
}
},
"640a8876-2e56-45c2-0bdc-7fadde58a6b7": {
"id": "640a8876-2e56-45c2-0bdc-7fadde58a6b7",
"type": "rectangle",
"name": "Rectangle",
"parentId": "page",
"childIndex": 3,
"point": [
237.27,
641.01
],
"size": [
1,
1
],
"rotation": 0,
"style": {
"color": "black",
"size": "small",
"isFilled": false,
"dash": "draw"
}
},
"5d8a0762-e20a-45e9-05ad-6bd407ecec8c": {
"id": "5d8a0762-e20a-45e9-05ad-6bd407ecec8c",
"type": "draw",
"name": "Draw",
"parentId": "page",
"childIndex": 4,
"point": [
362.41,
599.42
],
"rotation": 0,
"style": {
"color": "black",
"size": "small",
"isFilled": false,
"dash": "draw"
},
"points": [
[
0.24,
0,
0.5
]
],
"isComplete": true
}
},
"bindings": {}
}
},
"pageStates": {
"page": {
"id": "page",
"selectedIds": [],
"camera": {
"point": [
0,
0
],
"zoom": 1
},
"editingId": null
}
}
},
"assets": {}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 KiB

View file

View file

@ -0,0 +1,70 @@
{
"name": "wardlt",
"displayName": "Wardlt Editor",
"description": "The wardlt editor for VS Code",
"version": "0.10.0",
"publisher": "Wardlt",
"repository": "https://github.com/microsoft/vscode-extension-samples/helloworld-web-sample",
"engines": {
"vscode": "^1.59.0"
},
"categories": [
"Other"
],
"activationEvents": [
"onCustomEditor:tldraw.tldr",
"onCommand:tldraw.tldr.new"
],
"browser": "./dist/web/extension.js",
"main": "./dist/web/extension.js",
"extensionKind": [
"ui",
"workspace"
],
"contributes": {
"customEditors": [
{
"viewType": "tldraw.tldr",
"displayName": "Tldr Editor",
"selector": [
{
"filenamePattern": "*.tldr"
}
]
}
],
"commands": [
{
"command": "tldraw.tldr.new",
"title": "Create a new .tldr file",
"category": "Tldraw"
}
]
},
"scripts": {
"test": "vscode-test-web --browserType=chromium --extensionDevelopmentPath=. --extensionTestsPath=dist/web/test/suite/index.js",
"web": "vscode-test-web --browserType=chromium --extensionDevelopmentPath=.",
"pretest": "npm run compile-web",
"vscode:prepublish": "npm run package-web",
"compile-web": "webpack",
"watch-web": "webpack --watch",
"package-web": "webpack --mode production --devtool hidden-source-map",
"lint": "eslint src --ext ts"
},
"devDependencies": {
"@types/vscode": "^1.59.0",
"@types/mocha": "^9.0.0",
"eslint": "^7.32.0",
"@typescript-eslint/eslint-plugin": "^4.31.1",
"@typescript-eslint/parser": "^4.31.1",
"mocha": "^9.1.1",
"typescript": "^4.4.3",
"@vscode/test-web": "^0.0.12",
"ts-loader": "^9.2.5",
"webpack": "^5.52.1",
"webpack-cli": "^4.8.0",
"@types/webpack-env": "^1.16.2",
"assert": "^2.0.0",
"process": "^0.11.10"
}
}

View file

@ -0,0 +1,9 @@
import * as vscode from 'vscode'
import { TldrawEditorProvider } from './tldraw-editor'
// This is the extension entry point. This is called once on the first
// time a .tldr extension is opened/created.
export function activate(context: vscode.ExtensionContext) {
// Register our custom editor providers
context.subscriptions.push(TldrawEditorProvider.register(context))
}

View file

@ -0,0 +1,112 @@
import * as vscode from 'vscode'
/**
* Get the static html used for the editor webviews.
*
* IMPORTANT: Notice that how this runs while developing is much different than
* when deployed, review below to understand the differences.
*/
export function getHtmlForWebview(
context: vscode.ExtensionContext,
webview: vscode.Webview,
document: vscode.TextDocument
): string {
// For now we're going to tread badly formed .tldr files as freshly created files.
// This will happen if say a user creates a new .tldr file using New File or if they
// have a bad auto-merge that messes up the json of an existing .tldr file
// We pass null as the initialDocument value if we can't parse as json.
let documentContent
try {
JSON.parse(document.getText())
documentContent = document.getText()
} catch (error) {
documentContent = 'null'
}
return getDevModeHTML(context, webview, documentContent)
// return getProductionModeHTML(context, webview, documentContent);
}
/**
* In development we're leveraging the create-react-app based tooling to have niceties like
* live reload while developing the extension. The trick/hack is while the VS Code extension
* API requires an html content string to bootstrap the webview, if we provide the create-react-app
* page source as the content it will load all the right javascript files that handle live reloading
* and such.
*
* WARNING: This assumes the create-react-app's initial payload is unchanging, this may not be
* true when we do npm package updates of 'react-scripts' et al.
*/
function getDevModeHTML(
context: vscode.ExtensionContext,
webview: vscode.Webview,
documentContent: string
): string {
const host = 'http://localhost:5420'
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="stylesheet" href="${host}/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 app.</noscript>
<script>currentFile = ${documentContent};</script>
<script src="${host}/index.js"></script>
</body>
</html>`
}
/**
* WARNING!!!: This is not complete/working except when manually edited, which we've done once to successfully
* create a sideloadable vscode extension installer.
*
* For production mode we load the code/html from a statically built version of the create-react-app that hosts
* the tldraw/tldraw component based web app.
*
* TODO: In order to automate making this work, we'll need to somehow provide the extension the URLs of the
* build generated javascript/css files, their names change based on hashes of content at build time. I'm
* very out of date/rusty on my Typescript/React build tool ecosystem, so I've spun my tires a bit trying
* to figure out a non hacky way to do this. There is a asset-manifest.json file that includes the file
* paths, so
* 1) We need a way to fetch those during the build step,
* 2) ...or we need to figure out how to detect when we're running in production and read from the
* manifest file synchronously in this function. I suspect there is a chance expecting file access,
* especially synchronous file access will make the extension incompatible with the Github Codespaces
* client/server model
*/
function getProductionModeHTML(
context: vscode.ExtensionContext,
webview: vscode.Webview,
documentContent: string
): string {
const cssUrl = webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, 'editor-build/static/css', 'index.css')
)
const jsUrl = webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, 'editor-build/', 'index.js')
)
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="stylesheet" href="${cssUrl}" />
<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 app.</noscript>
<script>currentFile = ${documentContent};</script>
<script src="${jsUrl}""></script>
</body>
</html>
`
}

View file

@ -0,0 +1,29 @@
// imports mocha for the browser, defining the `mocha` global.
require('mocha/mocha')
export function run(): Promise<void> {
return new Promise((c, e) => {
mocha.setup({
ui: 'tdd',
reporter: undefined,
})
// bundles all files in the current directory matching `*.test`
const importAll = (r: __WebpackModuleApi.RequireContext) => r.keys().forEach(r)
importAll(require.context('.', true, /\.test$/))
try {
// Run the mocha test
mocha.run((failures) => {
if (failures > 0) {
e(new Error(`${failures} tests failed.`))
} else {
c()
}
})
} catch (err) {
console.error(err)
e(err)
}
})
}

View file

@ -0,0 +1,209 @@
import * as vscode from 'vscode'
import { getHtmlForWebview } from './get-html'
import { EXTENSION_EVENT, UI_EVENT } from './types'
/**
* The Tldraw extension's editor uses CustomTextEditorProvider, which means
* it's underlying model from VS Code's perspective is a text file. We likely
* will switch to CustomEditorProvider which gives us more control but will require
* more book keeping on our part.
*/
export class TldrawEditorProvider implements vscode.CustomTextEditorProvider {
private document?: vscode.TextDocument
// When the tldraw.tldr.new command is triggered, we need to provide a file
// name when generating a new .tldr file. newTldrawFileId's current value is
// added to the end of the file to make it unique, and then incremented.
//
// While there is probably a more thoughtful way of creating suggested file names,
// this name is only the temporary name for the new file. The file is still only in memory
// and hasn't been saved to an actual underlying file. If we suggest a name that turns
// out to already exist, VS Code will prevent it from being used in it's save dialogs.
private static newTldrawFileId = 1
// This is called one time by the main extension entry point. See 'extension.ts'.
// We register commands here and register our custom editor's provider telling VS Code
// that we can handle viewing/editing files with the .tldr extension.
public static register(context: vscode.ExtensionContext): vscode.Disposable {
// This makes a new command show up in the Command Palette that will
// create a new empty .tldr. The file will actually start out
// as an empty text file, which is fine as the editor treats
// blank text files as an empty Tldraw file. Once any change is made
// and the file saved it will be in a proper JSON format.
//
// The command shows up as: "Tldraw: Create a new .tldr file".
vscode.commands.registerCommand('tldraw.tldr.new', () => {
// This was included in the example CustomTextEditorProvider. It
// doesn't seem like we should need a workspace to be within to edit
// .tldr files, but I want to punt on digging into this.
// TODO: Test/decide if it is necessary to need a workspace.
// I can't think of why we'd want a concept of a global scope
// for editing .tldr files.
const workspaceFolders = vscode.workspace.workspaceFolders
if (!workspaceFolders) {
vscode.window.showErrorMessage(
'Creating new Tldraw Editor files currently requires opening a workspace'
)
return
}
// Create a placeholder name for the new file. A new file isn't actually
// created on disk yet, so this is just an in memory temporary name.
const uri = vscode.Uri.joinPath(
workspaceFolders[0].uri,
`drawing ${TldrawEditorProvider.newTldrawFileId++}.tldr`
).with({
scheme: 'untitled',
})
// This triggers VS Code to open our custom editor to edit the file.
// Note: Multiple editors can register to support certain files, so
// .tldr files might not by default open to our editor. In this case
// we are explicitly saying to launch our editor so we're streamlined. It
// may awkwardly ask if they want to use our editor or a text editor when
// first using our extension.
vscode.commands.executeCommand('vscode.openWith', uri, TldrawEditorProvider.viewType)
})
// This registers our editor provider, indicating to VS Code that we can
// handle files with the .tldr extension.
const provider = new TldrawEditorProvider(context)
const providerRegistration = vscode.window.registerCustomEditorProvider(
TldrawEditorProvider.viewType,
provider,
{
webviewOptions: {
// This is not optimal to set as true, but simplifies our intial implementation.
// If not set, VS Code will kill our editor instance whenever someone navigates to another
// file and it's hidden in it's own tab. VS Code requires you to implement some hooks
// to serialize/hydrate your editor state, but it's going to take probably some serious
// investigation to get the tldraw/tldraw components APIs ready to enable this. Talk to Francois for
// more details on this thinking.
retainContextWhenHidden: true,
},
// I'm not sure about the exact semantics about this one. I'm going to leave it in though as
// it sounds right for our needs. I think this ensures we get a unique instance of our provider
// per Tldraw editor tab, vs it being shared. It would be really cool if we could support
// multiple tabs sharing the same document state, but separate editor state (like zoom/pan/selection),
// but this will likely be a lot of work.
//
// The work to support this is likely very related to the comments above about the
// 'retainContextWhenHidden' flag as well as multiplayer support. Once we have more thought out
// support for distinguishing between the state that will be serialized and per-user/per-editor state
// this may become cheaper to implement.
supportsMultipleEditorsPerDocument: false,
}
)
return providerRegistration
}
// This is a unique identifier for our custom provider
private static readonly viewType = 'tldraw.tldr'
// We do nothing in our constructor for now
constructor(private readonly context: vscode.ExtensionContext) {}
/**
* Called when our custom editor is opened, this is where we need to configure
* the webview that each editor instance will live in. Webviews are basically iframes
* but usually have more functionality allowed than browsers without requiring users
* to approve a lot of security permissions. They can optionally even include the
* node.js runtime and APIs.
*
* Each opened .tldr file will have an assocated call to this.
*
* NOTE: I haven't tested what happens when you have two instances of the
* the same file open (say in two tabs split screened)
*/
public async resolveCustomTextEditor(
document: vscode.TextDocument,
webviewPanel: vscode.WebviewPanel,
_token: vscode.CancellationToken
): Promise<void> {
// Configure the webview. For now all we do is enable scripts and also
// provide the initial webview's html content.
webviewPanel.webview.options = {
enableScripts: true,
}
// See get-html.ts for more details, as the logic is a little more complicated
// than you think in order to have a good workflow while developing.
webviewPanel.webview.html = getHtmlForWebview(this.context, webviewPanel.webview, document)
function updateWebview() {
webviewPanel.webview.postMessage({
type: 'load',
text: document.getText(),
})
}
// I'm going to leave this code in as a reminder of this event, but disable it for now
//
// TODO: Revisit this function and think about how we want to respond to changes
// triggered by something other than the tldraw/tldraw component logic. An example
// being if the file changed on disk, say from git pull that pulls down a change
// to a .tldr file you have open in a tab.
const changeDocumentSubscription = vscode.workspace.onDidSaveTextDocument((e) => {
webviewPanel.webview.postMessage({
type: EXTENSION_EVENT.LOCAL_FILE_UPDATED,
text: document.getText(),
})
})
// Make sure we get rid of the listener when our editor is closed.
webviewPanel.onDidDispose(() => {
changeDocumentSubscription.dispose()
})
// Listen for posted messages asynchronously sent from the extensions webview code.
// For now there is only an update event, which is triggered when the tldraw/tldraw
// components document has changed.
webviewPanel.webview.onDidReceiveMessage((e) => {
switch (e.type) {
case UI_EVENT.TLDRAW_UPDATED: {
// Synchronize the TextDocument with the tldraw components document state
this.synchronizeTextDocument(document, JSON.parse(e.text))
break
}
}
})
// Send the initial document content to bootstrap the tldraw/tldraw component.
// Note: webview.postMessage is asynchronous and has the same semantics as
// when you post messages to an iframe from a parent window, in this case
// the extension isn't actually an enclosing web page.
webviewPanel.webview.postMessage({
type: EXTENSION_EVENT.INITIAL_DOCUMENT,
text: document.getText(),
})
}
/**
* This updates the vscode.TextDocument's in memory content to match the
* the stringified version of the provided json.
* VS Code will handle detecting if the in memory content and the on disk
* content are different, and then mark/unmark the tab as saved/unsaved
*/
private synchronizeTextDocument(document: vscode.TextDocument, json: any) {
// Just replace the entire document every time for this example extension.
// A more complete extension should compute minimal edits instead.
// TODO: Make sure to keep an eye on performance problems, as this may be the
// cause if the tldraw content is big or has been running for a long time.
// I'm not sure if VSCode is doing optimizations internally to detect/save
// patches of changes in the undo/redo buffer.
const edit = new vscode.WorkspaceEdit()
edit.replace(
document.uri,
new vscode.Range(0, 0, document.lineCount, 0),
JSON.stringify(json, null, 2)
)
return vscode.workspace.applyEdit(edit)
}
}

View file

@ -0,0 +1,12 @@
export enum UI_EVENT {
TLDRAW_UPDATED = 'TLDRAW_UPDATED',
}
export enum EXTENSION_EVENT {
INITIAL_DOCUMENT = 'INITIAL_DOCUMENT',
LOCAL_FILE_UPDATED = 'LOCAL_FILE_UPDATED',
}
export type UIMessage = MessageEvent<{ type: UI_EVENT; text: string }>
export type ExtensionMessage = MessageEvent<{ type: EXTENSION_EVENT; text: string }>

View file

@ -0,0 +1,9 @@
export function getNonce() {
let text = ''
const possible =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
for (let i = 0; i < 32; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length))
}
return text
}

View file

@ -0,0 +1,21 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"outDir": "dist",
"lib": [
"es6", "WebWorker"
],
"sourceMap": true,
"rootDir": "src",
"strict": true /* enable all strict type-checking options */
/* Additional Checks */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
},
"exclude": [
"node_modules",
".vscode-test-web"
]
}

View file

@ -0,0 +1,67 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
//@ts-check
'use strict';
//@ts-check
/** @typedef {import('webpack').Configuration} WebpackConfig **/
const path = require('path');
const webpack = require('webpack');
const webExtensionConfig = /** @type WebpackConfig */ {
mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production')
target: 'webworker', // extensions run in a webworker context
entry: {
extension: './src/web/extension.ts', // source of the web extension main file
'test/suite/index': './src/web/test/suite/index.ts', // source of the web extension test runner
},
output: {
filename: '[name].js',
path: path.join(__dirname, './dist/web'),
libraryTarget: 'commonjs',
},
resolve: {
mainFields: ['browser', 'module', 'main'], // look for `browser` entry point in imported node modules
extensions: ['.ts', '.js'], // support ts-files and js-files
alias: {
// provides alternate implementation for node module and source files
},
fallback: {
// Webpack 5 no longer polyfills Node.js core modules automatically.
// see https://webpack.js.org/configuration/resolve/#resolvefallback
// for the list of Node.js core module polyfills.
assert: require.resolve('assert'),
},
},
module: {
rules: [
{
test: /\.ts$/,
exclude: /node_modules/,
use: [
{
loader: 'ts-loader',
},
],
},
],
},
plugins: [
new webpack.ProvidePlugin({
process: 'process/browser', // provide a shim for the global `process` variable
}),
],
externals: {
vscode: 'commonjs vscode', // ignored because it doesn't exist
},
performance: {
hints: false,
},
devtool: 'nosources-source-map', // create a source map that points to the original source file
};
module.exports = [webExtensionConfig];

3158
vscode/extension/yarn.lock Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,6 @@
// This file isn't doing much; however it cannot be removed, or else
// the build will fail due to the fact that we're targeting ES6.
{ {
"presets": [ "presets": [
[ [