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:
416
apps/vscode/DOCS.md
Normal file
416
apps/vscode/DOCS.md
Normal file
@@ -0,0 +1,416 @@
|
||||
# VS Code Extension Documentation
|
||||
|
||||
## 1. Introduction
|
||||
|
||||
### What is the tldraw VS Code Extension?
|
||||
|
||||
The tldraw VS Code extension brings the full power of tldraw's infinite canvas directly into your code editor. You can create, view, and edit `.tldr` files seamlessly within VS Code, making it perfect for sketching ideas, creating diagrams, wireframes, and visual documentation alongside your code.
|
||||
|
||||
This extension provides a native editing experience that's fully compatible with tldraw.com, so you can start a drawing in VS Code and continue it in the browser, or vice versa.
|
||||
|
||||
### Installation
|
||||
|
||||
Install the extension directly from the VS Code marketplace:
|
||||
|
||||
1. Open VS Code
|
||||
2. Go to Extensions (Ctrl+Shift+X / Cmd+Shift+X)
|
||||
3. Search for "tldraw"
|
||||
4. Click Install
|
||||
|
||||
Alternatively, you can install from a `.vsix` file by running:
|
||||
|
||||
```bash
|
||||
code --install-extension tldraw-vscode.vsix
|
||||
```
|
||||
|
||||
### Quick Start
|
||||
|
||||
Here's how to get started with your first tldraw file:
|
||||
|
||||
```bash
|
||||
# Create a new .tldr file
|
||||
touch my-diagram.tldr
|
||||
|
||||
# Open it in VS Code - the tldraw editor will launch automatically
|
||||
code my-diagram.tldr
|
||||
```
|
||||
|
||||
You'll immediately have access to the full tldraw toolset: drawing, shapes, text, arrows, and more.
|
||||
|
||||
## 2. Core Features
|
||||
|
||||
### File Support
|
||||
|
||||
The extension provides comprehensive support for tldraw files:
|
||||
|
||||
**Supported File Types**
|
||||
|
||||
- `.tldr` files - Native tldraw format
|
||||
- `.tldr.json` files - JSON representation of tldraw documents
|
||||
|
||||
**File Operations**
|
||||
|
||||
- Create new tldraw files via Command Palette (`Cmd/Ctrl+Shift+P` → "tldraw: New Project")
|
||||
- Open existing `.tldr` files with automatic editor activation
|
||||
- Auto-save changes as you work
|
||||
- Full bidirectional compatibility with tldraw.com
|
||||
|
||||
### Drawing and Design Tools
|
||||
|
||||
You have access to tldraw's complete toolset within VS Code:
|
||||
|
||||
**Core Drawing Tools**
|
||||
|
||||
- **Select Tool** - Move, resize, and modify shapes
|
||||
- **Draw Tool** - Freehand drawing with pressure sensitivity support
|
||||
- **Eraser Tool** - Remove parts of drawings or entire shapes
|
||||
- **Hand Tool** - Pan around the infinite canvas
|
||||
|
||||
**Shape Creation**
|
||||
|
||||
- **Rectangle** - Perfect for wireframes and layouts
|
||||
- **Ellipse** - Circles and ovals for diagrams
|
||||
- **Arrow** - Connect ideas with labeled arrows
|
||||
- **Line** - Straight lines with various styles
|
||||
- **Text** - Rich text with formatting options
|
||||
- **Sticky Notes** - Great for brainstorming and annotations
|
||||
|
||||
**Advanced Features**
|
||||
|
||||
- Infinite canvas with smooth zoom and pan
|
||||
- Layer management and grouping
|
||||
- Shape styling (colors, fills, strokes)
|
||||
- Snap-to-grid and alignment tools
|
||||
- Undo/redo with full history
|
||||
|
||||
### Editor Integration
|
||||
|
||||
The extension integrates seamlessly with VS Code's interface:
|
||||
|
||||
**Custom Editor Provider**
|
||||
|
||||
- Native VS Code editor experience for `.tldr` files
|
||||
- Appears in editor tabs like any other file
|
||||
- Works with VS Code's split-pane layout
|
||||
- Respects VS Code's theme settings (light/dark mode)
|
||||
|
||||
**Keyboard Shortcuts**
|
||||
|
||||
- `Cmd/Ctrl +` - Zoom in
|
||||
- `Cmd/Ctrl -` - Zoom out
|
||||
- `Cmd/Ctrl 0` - Reset zoom to fit content
|
||||
- `Cmd/Ctrl D` - Toggle dark mode
|
||||
|
||||
**Command Palette Integration**
|
||||
|
||||
- "tldraw: New Project" - Create a new `.tldr` file
|
||||
- All commands prefixed with "tldraw:" for easy discovery
|
||||
|
||||
## 3. Working with Files
|
||||
|
||||
### Creating New Projects
|
||||
|
||||
You can create new tldraw files in several ways:
|
||||
|
||||
**Via Command Palette**
|
||||
|
||||
```bash
|
||||
# Open Command Palette
|
||||
Cmd/Ctrl + Shift + P
|
||||
|
||||
# Type and select
|
||||
tldraw: New Project
|
||||
```
|
||||
|
||||
This creates a new untitled `.tldr` file and opens it in the tldraw editor.
|
||||
|
||||
**Via File Explorer**
|
||||
|
||||
```bash
|
||||
# Create an empty .tldr file
|
||||
touch project-wireframes.tldr
|
||||
|
||||
# VS Code will automatically open it with the tldraw editor
|
||||
```
|
||||
|
||||
**Programmatically**
|
||||
You can also create `.tldr` files through VS Code's file system APIs if you're building extensions or automation.
|
||||
|
||||
### File Persistence and Auto-Save
|
||||
|
||||
The extension handles file persistence automatically:
|
||||
|
||||
**Auto-Save Behavior**
|
||||
|
||||
- Changes are automatically saved as you work
|
||||
- No need to manually save (Cmd/Ctrl+S) in most cases
|
||||
- File modification indicators work as expected in VS Code
|
||||
|
||||
**File Format**
|
||||
|
||||
- Files are stored in tldraw's native binary format
|
||||
- Optimized for performance and file size
|
||||
- Maintains full compatibility with tldraw.com
|
||||
|
||||
### Cross-Platform Compatibility
|
||||
|
||||
Your `.tldr` files work seamlessly across platforms:
|
||||
|
||||
**Browser Integration**
|
||||
|
||||
- Upload files directly to tldraw.com
|
||||
- Download files from tldraw.com to edit in VS Code
|
||||
- No conversion needed - files are fully compatible
|
||||
|
||||
**Sharing and Collaboration**
|
||||
|
||||
- Share `.tldr` files like any other project asset
|
||||
- Version control friendly (though binary diffs aren't human-readable)
|
||||
- Works great in shared repositories and project folders
|
||||
|
||||
## 4. Development and Customization
|
||||
|
||||
### Extension Architecture
|
||||
|
||||
The extension consists of two main components working together:
|
||||
|
||||
**Extension Process** (`apps/vscode/extension/`)
|
||||
|
||||
- Handles VS Code integration and file system operations
|
||||
- Manages the custom editor provider registration
|
||||
- Coordinates between VS Code APIs and the webview editor
|
||||
|
||||
**Webview Editor** (`apps/vscode/editor/`)
|
||||
|
||||
- React-based tldraw editor running in a webview
|
||||
- Full tldraw SDK implementation with complete feature set
|
||||
- Handles real-time drawing, user interactions, and state management
|
||||
|
||||
### Communication System
|
||||
|
||||
The extension uses a robust RPC (Remote Procedure Call) system for communication:
|
||||
|
||||
**Bidirectional Messaging**
|
||||
|
||||
```ts
|
||||
// Extension to webview
|
||||
webview.postMessage({
|
||||
type: 'openFile',
|
||||
data: { content: fileContent },
|
||||
})
|
||||
|
||||
// Webview to extension
|
||||
message.addEventListener('message', (event) => {
|
||||
if (event.data.type === 'fileChanged') {
|
||||
// Save changes to disk
|
||||
saveFile(event.data.content)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**File Change Synchronization**
|
||||
|
||||
- Real-time sync between editor state and file system
|
||||
- Automatic conflict resolution for external file changes
|
||||
- Efficient delta updates to minimize data transfer
|
||||
|
||||
### Hot Reload Development
|
||||
|
||||
For extension developers, the build system supports hot reload:
|
||||
|
||||
**Development Setup**
|
||||
|
||||
```bash
|
||||
# Start extension development with hot reload
|
||||
cd apps/vscode
|
||||
yarn dev
|
||||
|
||||
# This starts both extension and editor in watch mode
|
||||
# Extension reloads automatically when files change
|
||||
```
|
||||
|
||||
**Development Workflow**
|
||||
|
||||
1. Make changes to extension or editor code
|
||||
2. Extension automatically recompiles and reloads
|
||||
3. Test changes immediately in VS Code Extension Development Host
|
||||
4. No need to manually rebuild or restart
|
||||
|
||||
### External Content Handling
|
||||
|
||||
The extension can handle external content intelligently:
|
||||
|
||||
**Link Unfurling**
|
||||
|
||||
- Paste URLs to automatically create rich link previews
|
||||
- Supports common sites with Open Graph metadata
|
||||
- Configurable unfurling behavior
|
||||
|
||||
**Asset Management**
|
||||
|
||||
- Drag and drop images directly into drawings
|
||||
- Automatic asset optimization and caching
|
||||
- Support for various image formats
|
||||
|
||||
## 5. Advanced Usage
|
||||
|
||||
### Performance Optimization
|
||||
|
||||
The extension is optimized for performance in VS Code:
|
||||
|
||||
**Memory Management**
|
||||
|
||||
- Efficient webview lifecycle management
|
||||
- Automatic cleanup when files are closed
|
||||
- Optimized rendering for large documents
|
||||
|
||||
**File Loading**
|
||||
|
||||
- Lazy loading of large `.tldr` files
|
||||
- Progressive rendering for complex drawings
|
||||
- Background processing for file operations
|
||||
|
||||
**Zoom and Pan Performance**
|
||||
|
||||
- Hardware-accelerated rendering where available
|
||||
- Smooth interactions even with complex drawings
|
||||
- Efficient viewport culling for large canvases
|
||||
|
||||
### Integration with VS Code Features
|
||||
|
||||
The extension works well with VS Code's ecosystem:
|
||||
|
||||
**Multi-Root Workspaces**
|
||||
|
||||
- Full support for multi-root workspace configurations
|
||||
- Proper file path resolution across workspace folders
|
||||
- Consistent behavior regardless of workspace setup
|
||||
|
||||
**Split Editors**
|
||||
|
||||
- Open multiple `.tldr` files in split panes
|
||||
- Compare different versions side-by-side
|
||||
- Works with VS Code's editor group management
|
||||
|
||||
**Extension Compatibility**
|
||||
|
||||
- Compatible with other VS Code extensions
|
||||
- Respects VS Code's theme and color customizations
|
||||
- Works with productivity extensions like project managers
|
||||
|
||||
### Troubleshooting Common Issues
|
||||
|
||||
**File Won't Open**
|
||||
|
||||
- Ensure the file has a `.tldr` or `.tldr.json` extension
|
||||
- Check that the file isn't corrupted or empty
|
||||
- Try creating a new file to test the extension
|
||||
|
||||
**Performance Issues**
|
||||
|
||||
- Close unused `.tldr` files to free memory
|
||||
- Restart VS Code if webviews become unresponsive
|
||||
- Check VS Code's output panel for error messages
|
||||
|
||||
**Sync Issues with tldraw.com**
|
||||
|
||||
- Verify file format compatibility
|
||||
- Try re-saving the file in VS Code
|
||||
- Check for any file permission issues
|
||||
|
||||
## 6. Building and Distribution
|
||||
|
||||
### Development Build
|
||||
|
||||
To build the extension locally:
|
||||
|
||||
**Prerequisites**
|
||||
|
||||
- Node.js 16+ and yarn
|
||||
- VS Code development environment
|
||||
|
||||
**Build Process**
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
cd apps/vscode
|
||||
yarn install
|
||||
|
||||
# Build extension and editor
|
||||
yarn build
|
||||
|
||||
# Package for distribution
|
||||
yarn package
|
||||
```
|
||||
|
||||
This creates a `.vsix` file that can be installed locally or distributed.
|
||||
|
||||
**Development Testing**
|
||||
|
||||
```bash
|
||||
# Start development environment
|
||||
yarn dev
|
||||
|
||||
# This opens VS Code Extension Development Host
|
||||
# Test your changes in the new VS Code window
|
||||
```
|
||||
|
||||
### Publishing
|
||||
|
||||
The extension supports multiple distribution channels:
|
||||
|
||||
**VS Code Marketplace**
|
||||
|
||||
- Automated publishing from CI/CD pipeline
|
||||
- Version tagging based on git branches
|
||||
- Pre-release builds available for testing
|
||||
|
||||
**Manual Installation**
|
||||
|
||||
```bash
|
||||
# Install from local .vsix file
|
||||
code --install-extension tldraw-vscode-*.vsix
|
||||
|
||||
# Or drag the .vsix file into VS Code Extensions view
|
||||
```
|
||||
|
||||
**GitHub Releases**
|
||||
|
||||
- Direct `.vsix` downloads from repository releases
|
||||
- Includes release notes and compatibility information
|
||||
- Tagged versions for stable releases
|
||||
|
||||
### Configuration and Settings
|
||||
|
||||
The extension supports customization through VS Code settings:
|
||||
|
||||
**Available Settings**
|
||||
|
||||
- Theme preferences (auto-detect from VS Code)
|
||||
- Default canvas size and grid settings
|
||||
- Auto-save behavior configuration
|
||||
- Performance optimization toggles
|
||||
|
||||
**Settings Access**
|
||||
|
||||
```json
|
||||
{
|
||||
"tldraw.theme": "auto",
|
||||
"tldraw.autoSave": true,
|
||||
"tldraw.gridSize": 20
|
||||
}
|
||||
```
|
||||
|
||||
Access these through VS Code's Settings UI or directly in `settings.json`.
|
||||
|
||||
## Quality and Best Practices
|
||||
|
||||
This extension follows VS Code's development best practices:
|
||||
|
||||
- **Accessibility** - Full keyboard navigation and screen reader support
|
||||
- **Performance** - Efficient resource usage and memory management
|
||||
- **Security** - Safe handling of user content and external resources
|
||||
- **Internationalization** - Ready for localization and global users
|
||||
- **Error Handling** - Graceful degradation and helpful error messages
|
||||
|
||||
The extension provides a professional-grade drawing experience that integrates seamlessly with your development workflow, making visual thinking and documentation a natural part of your coding process.
|
||||
114
apps/vscode/README.md
Normal file
114
apps/vscode/README.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# VS Code extension
|
||||
|
||||
This folder contains the source for the tldraw VS Code extension.
|
||||
|
||||
## Developing
|
||||
|
||||
## 1. Install dependencies
|
||||
|
||||
- Run `yarn` from the root folder
|
||||
|
||||
## 2. Start the editor
|
||||
|
||||
In the root folder:
|
||||
|
||||
- Run `yarn dev-vscode`.
|
||||
|
||||
This will start the development server for the `apps/vscode/editor` project and open the `apps/vscode/extension` folder in a new VS Code window.
|
||||
|
||||
In the `apps/vscode/extension` window, open the terminal and:
|
||||
|
||||
- Install dependencies (`yarn`)
|
||||
- Start the VS Code debugger (`Menu > Run > Start Debugging` or by pressing `F5`). This will open another VS Code window with the extension running.
|
||||
|
||||
Open a `.tldr` file from the file explorer or create a new `.tldr` file from the command palette.
|
||||
|
||||
## 3. Debugging
|
||||
|
||||
You can use standard debugging techniques like `console.log`, which will be displayed in the VS Code window with the extension running. It will display logs both from the Extension and the Editor. VS Code editor with the Extension folder will show more detailed logs from the Extension project. You can also use a debugger.
|
||||
|
||||
The code is hot-reloaded, so the developer experience is quite nice.
|
||||
|
||||
## Publishing
|
||||
|
||||
Update the version in the `apps/vscode/extension/package.json`. Update the `apps/vscode/extension/CHANGELOG.md` with the new version number and the changes.
|
||||
|
||||
To publish:
|
||||
|
||||
- Install `vsce` globally
|
||||
- Run `vsce login tldraw-org` and sign in. For this to work you need to create a [personal access token](https://code.visualstudio.com/api/working-with-extensions/publishing-extension#get-a-personal-access-token) and you also need to be added to the `tldraw-org` organization on the [Visual Studio Marketplace](https://marketplace.visualstudio.com/manage).
|
||||
|
||||
In the `apps/vscode/extension` folder:
|
||||
|
||||
- Run `yarn package`
|
||||
- Run `yarn publish`
|
||||
|
||||
## Project overview
|
||||
|
||||
The Visual Studio Code extension is made of two projects:
|
||||
|
||||
### 1. Extension project
|
||||
|
||||
Extension project is under `apps/vscode/extension` and contains the code needed to run a VS Code Extension - it implements the required VS Code interfaces so that VS Code can call our extension and start running it.
|
||||
|
||||
It registers the command for generating a new `.tldr` file, custom editor for `.tldr` files, and it communicates with the WebViews that run `@tldraw/editor` (more on this later on).
|
||||
|
||||
VS Code Extension API offers two ways for adding [new editors](https://code.visualstudio.com/api/extension-guides/custom-editors): `CustomEditor` and `CustomTextEditor`. We are using [`CustomEditor`](https://code.visualstudio.com/api/extension-guides/custom-editors#custom-editor), even though it means we have to do a bit more work and maintain the contents of the document ourselves. This allows us to better support features like `undo`, `redo`, and `revert`, since we are in complete control of the contents of the document.
|
||||
|
||||
The custom editor logic lives in `TldrawDocument`, where we handle all the required custom editor operations like reading the file from disk, saving the file, backups, reverting, etc. When a `.tldr` file is opened a new instance of a `TldrawDocument` is created and this instance then serves as the underlying document model for displaying in the VS Code editors for editing this file. You can open the same file in multiple editors, but even then only a single instance of `TldrawDocument` is created per file.
|
||||
|
||||
When a users opens a file a new WebView is created by the `TldrawWebviewManager` and the file's contents are sent do it. WebViews then show our editor project, which is described below.
|
||||
|
||||
### 2. Editor project
|
||||
|
||||
Editor project is under `apps/vscode/editor`. When a file is opened a new instance of a WebView is created and we show `@tldraw/editor` this WebView.
|
||||
|
||||
The implementation is pretty straight forward, but there are some limitations of running `tldraw` inside a WebView, like `window.open` and `window.prompt` not being available, as well as some issues with embeds. We are using `useLocalSyncClient` to sync between different editor instances for cases when the same file is opened in multiple editors.
|
||||
|
||||
When users interact with tldraw we listen for changes and when changes happen we serialize the document contents and send them over to `TldrawDocument`. This makes VS Code aware of the changes and allows users to use built in features like `save`, `save as`, `undo`, `redo`, and `revert`.
|
||||
|
||||
### Overview of the communication between VS Code, Extension, and the Editor
|
||||
|
||||
VS Code activates our extension when needed - when a user opens the first `.tldr` file or when a user runs our registered command. Then, VS Code calls into `TldrawEditorProvider` to open the custom editor, which in turn creates a `TldrawDocument` instance. We read the file contents from disk and send them to the WebView, which then shows the Editor. When the user interacts with the editor we send the changes back to the Extension, which then updates the `TldrawDocument` instance. Since the instance is always kept up to date we can correctly handle user actions like `save`, `save as`, `undo`, `redo`, and `revert`.
|
||||
|
||||

|
||||
|
||||
#### 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)
|
||||
|
||||
## Distributions
|
||||
|
||||
You can find tldraw on npm [here](https://www.npmjs.com/package/@tldraw/tldraw?activeTab=versions).
|
||||
|
||||
## Contribution
|
||||
|
||||
Found a bug? Please [submit an issue](https://github.com/tldraw/tldraw/issues/new).
|
||||
|
||||
## License
|
||||
|
||||
The code in this folder is Copyright (c) 2024-present tldraw Inc. 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).
|
||||
1
apps/vscode/VS-Code-Extension-1.tldr
Normal file
1
apps/vscode/VS-Code-Extension-1.tldr
Normal file
File diff suppressed because one or more lines are too long
9
apps/vscode/editor/LICENSE.md
Normal file
9
apps/vscode/editor/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.
|
||||
23
apps/vscode/editor/README.md
Normal file
23
apps/vscode/editor/README.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# @tldraw/vscode-editor
|
||||
|
||||
This folder contains the React app used by the tldraw VS Code extension.
|
||||
|
||||
## 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/editor/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).
|
||||
57
apps/vscode/editor/package.json
Normal file
57
apps/vscode/editor/package.json
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"name": "@tldraw/vscode-editor",
|
||||
"description": "An an editor for the tldraw vscode extension.",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"author": {
|
||||
"name": "tldraw Inc.",
|
||||
"email": "hello@tldraw.com"
|
||||
},
|
||||
"homepage": "https://tldraw.dev",
|
||||
"license": "MIT",
|
||||
"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"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "yarn run -T tsx scripts/build.ts",
|
||||
"dev": "yarn run -T tsx scripts/dev.ts",
|
||||
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist",
|
||||
"lint": "yarn run -T tsx ../../../internal/scripts/lint.ts",
|
||||
"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/assets": "workspace:*",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/node": "^22.15.31",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"concurrently": "^9.1.2",
|
||||
"create-serve": "1.0.1",
|
||||
"dotenv": "^16.4.7",
|
||||
"esbuild": "^0.28.0",
|
||||
"fs-extra": "^11.3.0",
|
||||
"lazyrepo": "0.0.0-alpha.27",
|
||||
"react": "^19.2.1",
|
||||
"react-dom": "^19.2.1",
|
||||
"tldraw": "workspace:*",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"gitHead": "a7dac0f83ad998e205c2aab58182cb4ba4e099a6"
|
||||
}
|
||||
46
apps/vscode/editor/public/index.css
Normal file
46
apps/vscode/editor/public/index.css
Normal file
@@ -0,0 +1,46 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@500;700&display=swap');
|
||||
|
||||
html,
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
overscroll-behavior: none;
|
||||
touch-action: none;
|
||||
overflow: hidden;
|
||||
min-height: 100vh;
|
||||
/* mobile viewport bug fix */
|
||||
min-height: -webkit-fill-available;
|
||||
}
|
||||
|
||||
#root {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tldraw--editor {
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background-color: var(--tl-color-background);
|
||||
}
|
||||
58
apps/vscode/editor/scripts/build.ts
Normal file
58
apps/vscode/editor/scripts/build.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import esbuild from 'esbuild'
|
||||
import fse, { exists } from 'fs-extra'
|
||||
import { logEnv } from './cli'
|
||||
import { getDirname } from './path'
|
||||
|
||||
const rootDir = getDirname(import.meta.url, '../')
|
||||
const log = logEnv('editor')
|
||||
|
||||
export async function build() {
|
||||
try {
|
||||
const targetFolder = `${rootDir}dist/`
|
||||
if (await exists(targetFolder)) {
|
||||
log({ cmd: 'remove', args: { target: targetFolder } })
|
||||
await fs.promises.rm(targetFolder, { recursive: true })
|
||||
}
|
||||
|
||||
await fs.promises.mkdir(targetFolder)
|
||||
|
||||
const topSource = `${rootDir}public`
|
||||
const files = await fs.promises.readdir(topSource)
|
||||
for (const file of files) {
|
||||
const dest = targetFolder + path.basename(file)
|
||||
const source = path.join(topSource, file)
|
||||
log({ cmd: 'copy', args: { source, dest } })
|
||||
await fse.copy(source, dest)
|
||||
}
|
||||
const entryPoints = [`${rootDir}src/index.tsx`]
|
||||
|
||||
log({ cmd: 'esbuild', args: { entryPoints } })
|
||||
esbuild.buildSync({
|
||||
entryPoints,
|
||||
outfile: `${rootDir}/dist/index.js`,
|
||||
minify: false,
|
||||
bundle: true,
|
||||
target: 'es6',
|
||||
jsxFactory: 'React.createElement',
|
||||
jsxFragment: 'React.Fragment',
|
||||
loader: {
|
||||
'.woff2': 'dataurl',
|
||||
'.woff': 'dataurl',
|
||||
'.svg': 'file',
|
||||
'.png': 'file',
|
||||
'.json': 'file',
|
||||
},
|
||||
define: {
|
||||
'process.env.NODE_ENV': '"production"',
|
||||
},
|
||||
})
|
||||
log({ cmd: 'esbuild:success', args: {} })
|
||||
} catch (error) {
|
||||
log({ cmd: 'esbuild:error', args: { error } })
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
build()
|
||||
58
apps/vscode/editor/scripts/cli.ts
Normal file
58
apps/vscode/editor/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 })
|
||||
}
|
||||
}
|
||||
77
apps/vscode/editor/scripts/dev.ts
Normal file
77
apps/vscode/editor/scripts/dev.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import dotenv from 'dotenv'
|
||||
import esbuild from 'esbuild'
|
||||
import fse, { exists } from 'fs-extra'
|
||||
import { logEnv } from './cli'
|
||||
import { copyEditor } from './helpers'
|
||||
import { getDirname } from './path'
|
||||
|
||||
dotenv.config()
|
||||
const rootDir = getDirname(import.meta.url, '../')
|
||||
const log = logEnv('editor')
|
||||
|
||||
export async function run() {
|
||||
try {
|
||||
const targetFolder = `${rootDir}dist/`
|
||||
if (await exists(targetFolder)) {
|
||||
log({ cmd: 'remove', args: { target: targetFolder } })
|
||||
await fs.promises.rm(targetFolder, { recursive: true })
|
||||
}
|
||||
|
||||
await fs.promises.mkdir(targetFolder)
|
||||
|
||||
const topSource = `${rootDir}public`
|
||||
const files = await fs.promises.readdir(topSource)
|
||||
for (const file of files) {
|
||||
const dest = targetFolder + path.basename(file)
|
||||
const source = path.join(topSource, file)
|
||||
log({ cmd: 'copy', args: { source, dest } })
|
||||
await fse.copy(source, dest)
|
||||
}
|
||||
const entryPoints = [`${rootDir}src/index.tsx`]
|
||||
|
||||
log({ cmd: 'esbuild', args: { entryPoints } })
|
||||
|
||||
const builder = await esbuild.context({
|
||||
entryPoints,
|
||||
outfile: `${rootDir}/dist/index.js`,
|
||||
minify: false,
|
||||
bundle: true,
|
||||
target: 'es6',
|
||||
jsxFactory: 'React.createElement',
|
||||
jsxFragment: 'React.Fragment',
|
||||
loader: {
|
||||
'.woff2': 'dataurl',
|
||||
'.woff': 'dataurl',
|
||||
'.svg': 'file',
|
||||
'.png': 'file',
|
||||
'.json': 'file',
|
||||
},
|
||||
define: {
|
||||
'process.env.NODE_ENV': '"development"',
|
||||
},
|
||||
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: {} })
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
await builder.watch()
|
||||
} catch (error) {
|
||||
log({ cmd: 'esbuild:error', args: { error } })
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
run()
|
||||
25
apps/vscode/editor/scripts/helpers.ts
Normal file
25
apps/vscode/editor/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 })
|
||||
}
|
||||
}
|
||||
16
apps/vscode/editor/scripts/path.ts
Normal file
16
apps/vscode/editor/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
|
||||
}
|
||||
}
|
||||
69
apps/vscode/editor/src/ChangeResponder.tsx
Normal file
69
apps/vscode/editor/src/ChangeResponder.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
debounce,
|
||||
parseAndLoadDocument,
|
||||
serializeTldrawJson,
|
||||
useDefaultHelpers,
|
||||
useEditor,
|
||||
} from 'tldraw'
|
||||
// @ts-ignore
|
||||
import type { VscodeMessage } from '../../messages'
|
||||
import '../public/index.css'
|
||||
import { vscode } from './utils/vscode'
|
||||
|
||||
export function ChangeResponder() {
|
||||
const editor = useEditor()
|
||||
const { addToast, clearToasts, msg } = useDefaultHelpers()
|
||||
|
||||
React.useEffect(() => {
|
||||
// When a message is received from the VS Code extension, handle it
|
||||
function handleMessage({ data: message }: MessageEvent<VscodeMessage>) {
|
||||
switch (message.type) {
|
||||
// case 'vscode:undo': {
|
||||
// editor.undo()
|
||||
// break
|
||||
// }
|
||||
// case 'vscode:redo': {
|
||||
// editor.redo()
|
||||
// break
|
||||
// }
|
||||
case 'vscode:revert': {
|
||||
parseAndLoadDocument(editor, message.data.fileContents, msg, addToast)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('message', handleMessage)
|
||||
|
||||
return () => {
|
||||
clearToasts()
|
||||
window.removeEventListener('message', handleMessage)
|
||||
}
|
||||
}, [editor, msg, addToast, clearToasts])
|
||||
|
||||
React.useEffect(() => {
|
||||
// When the history changes, send the new file contents to VSCode
|
||||
const handleChange = debounce(async () => {
|
||||
vscode.postMessage({
|
||||
type: 'vscode:editor-updated',
|
||||
data: {
|
||||
fileContents: await serializeTldrawJson(editor),
|
||||
},
|
||||
})
|
||||
}, 250)
|
||||
|
||||
vscode.postMessage({
|
||||
type: 'vscode:editor-loaded',
|
||||
})
|
||||
|
||||
const dispose = editor.store.listen(handleChange, { scope: 'document' })
|
||||
|
||||
return () => {
|
||||
handleChange()
|
||||
dispose()
|
||||
}
|
||||
}, [editor])
|
||||
|
||||
return null
|
||||
}
|
||||
44
apps/vscode/editor/src/FileOpen.tsx
Normal file
44
apps/vscode/editor/src/FileOpen.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React from 'react'
|
||||
import { parseAndLoadDocument, useDefaultHelpers, useEditor } from 'tldraw'
|
||||
import { vscode } from './utils/vscode'
|
||||
|
||||
export function FileOpen({
|
||||
fileContents,
|
||||
forceDarkMode,
|
||||
}: {
|
||||
fileContents: string
|
||||
forceDarkMode: boolean
|
||||
}) {
|
||||
const editor = useEditor()
|
||||
const { msg, addToast, clearToasts } = useDefaultHelpers()
|
||||
const [isFileLoaded, setIsFileLoaded] = React.useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isFileLoaded) return
|
||||
function onV1FileLoad() {
|
||||
vscode.postMessage({
|
||||
type: 'vscode:v1-file-opened',
|
||||
data: {
|
||||
description: msg('vscode.file-open.desc'),
|
||||
backup: msg('vscode.file-open.backup'),
|
||||
backupSaved: msg('vscode.file-open.backup-saved'),
|
||||
backupFailed: msg('vscode.file-open.backup-failed'),
|
||||
dontAskAgain: msg('vscode.file-open.dont-show-again'),
|
||||
open: msg('vscode.file-open.open'),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function loadFile() {
|
||||
await parseAndLoadDocument(editor, fileContents, msg, addToast, onV1FileLoad, forceDarkMode)
|
||||
}
|
||||
|
||||
loadFile()
|
||||
setIsFileLoaded(true)
|
||||
return () => {
|
||||
clearToasts()
|
||||
}
|
||||
}, [fileContents, editor, addToast, msg, clearToasts, forceDarkMode, isFileLoaded])
|
||||
|
||||
return null
|
||||
}
|
||||
17
apps/vscode/editor/src/FullPageMessage.tsx
Normal file
17
apps/vscode/editor/src/FullPageMessage.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
export function FullPageMessage({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'var(--vscode-editor-background)',
|
||||
color: 'var(--vscode-editor-foreground)',
|
||||
position: 'fixed',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
WebkitTransform: 'translate(-50%, -50%)',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
41
apps/vscode/editor/src/Links.tsx
Normal file
41
apps/vscode/editor/src/Links.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { TldrawUiMenuGroup, TldrawUiMenuItem } from 'tldraw'
|
||||
import { openUrl } from './utils/url'
|
||||
|
||||
export function Links() {
|
||||
return (
|
||||
<>
|
||||
<TldrawUiMenuGroup id="links">
|
||||
<TldrawUiMenuItem
|
||||
id="about"
|
||||
label="help-menu.terms"
|
||||
readonlyOk
|
||||
onSelect={() => {
|
||||
openUrl(
|
||||
'https://github.com/tldraw/tldraw/blob/main/apps/dotcom/client/TERMS_OF_SERVICE.md'
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<TldrawUiMenuItem
|
||||
id="about"
|
||||
label="help-menu.privacy"
|
||||
readonlyOk
|
||||
onSelect={() => {
|
||||
openUrl(
|
||||
'https://github.com/tldraw/tldraw/blob/main/apps/dotcom/client/PRIVACY_POLICY.md'
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</TldrawUiMenuGroup>
|
||||
<TldrawUiMenuGroup id="tldraw">
|
||||
<TldrawUiMenuItem
|
||||
id="about"
|
||||
label="help-menu.about"
|
||||
readonlyOk
|
||||
onSelect={() => {
|
||||
openUrl('https://tldraw.dev')
|
||||
}}
|
||||
/>
|
||||
</TldrawUiMenuGroup>
|
||||
</>
|
||||
)
|
||||
}
|
||||
153
apps/vscode/editor/src/app.tsx
Normal file
153
apps/vscode/editor/src/app.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
import { getAssetUrlsByImport } from '@tldraw/assets/imports'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
DefaultMainMenu,
|
||||
DefaultSpinner,
|
||||
EditSubmenu,
|
||||
Editor,
|
||||
ErrorBoundary,
|
||||
ExportFileContentSubMenu,
|
||||
ExtrasGroup,
|
||||
PreferencesGroup,
|
||||
TLComponents,
|
||||
Tldraw,
|
||||
ViewSubmenu,
|
||||
hardReset,
|
||||
setRuntimeOverrides,
|
||||
} from 'tldraw'
|
||||
import 'tldraw/tldraw.css'
|
||||
import { VscodeMessage } from '../../messages'
|
||||
import '../public/index.css'
|
||||
import { ChangeResponder } from './ChangeResponder'
|
||||
import { FileOpen } from './FileOpen'
|
||||
import { FullPageMessage } from './FullPageMessage'
|
||||
import { Links } from './Links'
|
||||
import { onCreateAssetFromUrl } from './utils/bookmarks'
|
||||
import { registerExternalUrlContentHandler } from './utils/externalUrlContentHandler'
|
||||
import { vscode } from './utils/vscode'
|
||||
|
||||
setRuntimeOverrides({
|
||||
openWindow: (url, target) => {
|
||||
if (url.includes('utm_campaign=watermark')) {
|
||||
url = url.replace('utm_source=sdk', 'utm_source=extension')
|
||||
}
|
||||
vscode.postMessage({
|
||||
type: 'vscode:open-window',
|
||||
data: {
|
||||
url,
|
||||
target,
|
||||
},
|
||||
})
|
||||
},
|
||||
refreshPage: () => {
|
||||
vscode.postMessage({
|
||||
type: 'vscode:refresh-page',
|
||||
})
|
||||
},
|
||||
hardReset: async () => {
|
||||
await hardReset({ shouldReload: false })
|
||||
vscode.postMessage({
|
||||
type: 'vscode:hard-reset',
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const handleError = (error: any) => {
|
||||
console.error(error.message)
|
||||
}
|
||||
|
||||
export function WrappedTldrawEditor() {
|
||||
return (
|
||||
<div className="tldraw--editor">
|
||||
<ErrorBoundary
|
||||
fallback={() => <FullPageMessage>Fallback</FullPageMessage>}
|
||||
onError={handleError}
|
||||
>
|
||||
<TldrawWrapper />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function TldrawWrapper() {
|
||||
const [tldrawInnerProps, setTldrawInnerProps] = useState<TLDrawInnerProps | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
function handleMessage({ data: message }: MessageEvent<VscodeMessage>) {
|
||||
switch (message.type) {
|
||||
case 'vscode:opened-file': {
|
||||
setTldrawInnerProps({
|
||||
assetSrc: message.data.assetSrc,
|
||||
fileContents: message.data.fileContents,
|
||||
uri: message.data.uri,
|
||||
isDarkMode: message.data.isDarkMode,
|
||||
})
|
||||
// We only want to listen for this message once
|
||||
window.removeEventListener('message', handleMessage)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('message', handleMessage)
|
||||
|
||||
vscode.postMessage({ type: 'vscode:ready-to-receive-file' })
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('message', handleMessage)
|
||||
}
|
||||
}, [setTldrawInnerProps])
|
||||
|
||||
return tldrawInnerProps === null ? (
|
||||
<FullPageMessage>
|
||||
<DefaultSpinner />
|
||||
</FullPageMessage>
|
||||
) : (
|
||||
<TldrawInner {...tldrawInnerProps} />
|
||||
)
|
||||
}
|
||||
|
||||
export interface TLDrawInnerProps {
|
||||
assetSrc: string
|
||||
fileContents: string
|
||||
uri: string
|
||||
isDarkMode: boolean
|
||||
}
|
||||
|
||||
const components: TLComponents = {
|
||||
MainMenu: () => (
|
||||
<DefaultMainMenu>
|
||||
<EditSubmenu />
|
||||
<ViewSubmenu />
|
||||
<ExportFileContentSubMenu />
|
||||
<ExtrasGroup />
|
||||
<PreferencesGroup />
|
||||
<Links />
|
||||
</DefaultMainMenu>
|
||||
),
|
||||
}
|
||||
function TldrawInner({ uri, assetSrc, isDarkMode, fileContents }: TLDrawInnerProps) {
|
||||
const assetUrls = useMemo(() => getAssetUrlsByImport({ baseUrl: assetSrc }), [assetSrc])
|
||||
|
||||
const handleMount = useCallback((editor: Editor) => {
|
||||
editor.registerExternalAssetHandler('url', onCreateAssetFromUrl)
|
||||
registerExternalUrlContentHandler(editor)
|
||||
}, [])
|
||||
|
||||
const licenseKey =
|
||||
'tldraw-tldraw-2027-05-22/WyJDOEVmdnNlQyIsWyJ2c2NvZGUtd2VidmlldzpcXC9cXC8uKnRsZHJhdy1vcmdcXC50bGRyYXctdnNjb2RlIl0sNDEsIjIwMjctMDUtMjIiXQ.s/4f2jMqZhK2eAHT0LjZZ48NAcrsZ7UdWMerUzQ6q8SZtpkMmuv3JoPviFo+xkfNxD4eaGTGWgHIYyX1VCbwXg'
|
||||
return (
|
||||
<Tldraw
|
||||
assetUrls={assetUrls}
|
||||
persistenceKey={uri}
|
||||
onMount={handleMount}
|
||||
components={components}
|
||||
licenseKey={licenseKey}
|
||||
>
|
||||
{/* <DarkModeHandler themeKind={themeKind} /> */}
|
||||
|
||||
<FileOpen fileContents={fileContents} forceDarkMode={isDarkMode} />
|
||||
<ChangeResponder />
|
||||
</Tldraw>
|
||||
)
|
||||
}
|
||||
9
apps/vscode/editor/src/index.tsx
Normal file
9
apps/vscode/editor/src/index.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import * as React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import { WrappedTldrawEditor } from './app'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<WrappedTldrawEditor />
|
||||
</React.StrictMode>
|
||||
)
|
||||
70
apps/vscode/editor/src/utils/bookmarks.ts
Normal file
70
apps/vscode/editor/src/utils/bookmarks.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { AssetRecordType, TLAsset, TLUrlExternalContent, fetch, getHashForString } from 'tldraw'
|
||||
import { rpc } from './rpc'
|
||||
|
||||
export async function onCreateAssetFromUrl({ url }: TLUrlExternalContent): Promise<TLAsset> {
|
||||
try {
|
||||
// First, try to get the data from the extension manager process, using node's fetch
|
||||
const meta = await rpc('vscode:bookmark', { url })
|
||||
|
||||
return {
|
||||
id: AssetRecordType.createId(getHashForString(url)),
|
||||
typeName: 'asset',
|
||||
type: 'bookmark',
|
||||
props: {
|
||||
src: url,
|
||||
description: meta.description ?? '',
|
||||
image: meta.image ?? '',
|
||||
favicon: meta.favicon ?? '',
|
||||
title: meta.title ?? '',
|
||||
},
|
||||
meta: {},
|
||||
}
|
||||
} catch {
|
||||
// Otherwise, fallback to fetching data from the url
|
||||
|
||||
let meta: { image: string; favicon: string; title: string; description: string }
|
||||
|
||||
try {
|
||||
const resp = await fetch(url, {
|
||||
method: 'GET',
|
||||
mode: 'no-cors',
|
||||
})
|
||||
const html = await resp.text()
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html')
|
||||
meta = {
|
||||
image: doc.head.querySelector('meta[property="og:image"]')?.getAttribute('content') ?? '',
|
||||
favicon:
|
||||
doc.head.querySelector('link[rel="apple-touch-icon"]')?.getAttribute('href') ??
|
||||
doc.head.querySelector('link[rel="icon"]')?.getAttribute('href') ??
|
||||
'',
|
||||
title: doc.head.querySelector('meta[property="og:title"]')?.getAttribute('content') ?? '',
|
||||
description:
|
||||
doc.head.querySelector('meta[property="og:description"]')?.getAttribute('content') ?? '',
|
||||
}
|
||||
if (meta.image && !meta.image.startsWith('http')) {
|
||||
meta.image = new URL(meta.image, url).href
|
||||
}
|
||||
if (meta.favicon && !meta.favicon.startsWith('http')) {
|
||||
meta.favicon = new URL(meta.favicon, url).href
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
meta = { image: '', favicon: '', title: '', description: '' }
|
||||
}
|
||||
|
||||
// Create the bookmark asset from the meta
|
||||
return {
|
||||
id: AssetRecordType.createId(getHashForString(url)),
|
||||
typeName: 'asset',
|
||||
type: 'bookmark',
|
||||
props: {
|
||||
src: url,
|
||||
image: meta.image,
|
||||
favicon: meta.favicon,
|
||||
title: meta.title,
|
||||
description: meta.description,
|
||||
},
|
||||
meta: {},
|
||||
}
|
||||
}
|
||||
}
|
||||
25
apps/vscode/editor/src/utils/externalUrlContentHandler.ts
Normal file
25
apps/vscode/editor/src/utils/externalUrlContentHandler.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Editor } from 'tldraw'
|
||||
import { rpc } from './rpc'
|
||||
|
||||
export function registerExternalUrlContentHandler(editor: Editor) {
|
||||
const currentFilesContentHandler = editor.externalContentHandlers['files']
|
||||
const currentUrlContentHandler = editor.externalContentHandlers['url']
|
||||
editor.registerExternalContentHandler('url', async (info) => {
|
||||
// This happens when you shift drag a file from the file explorer to the editor
|
||||
if (info.url.startsWith('file://')) {
|
||||
// We cannot fetch the file directly since we are sandboxed, we have to ask the extension manager to get it for us
|
||||
const data = await rpc('vscode:get-file', { url: info.url })
|
||||
const uint8Array = new Uint8Array(data.file)
|
||||
const blob = new Blob([uint8Array], { type: data.mimeType })
|
||||
const file = new File([blob], data.fileName, { type: data.mimeType })
|
||||
currentFilesContentHandler?.({
|
||||
type: 'files',
|
||||
files: [file],
|
||||
ignoreParent: false,
|
||||
})
|
||||
} else {
|
||||
// default logic if it's a regular url
|
||||
currentUrlContentHandler?.(info)
|
||||
}
|
||||
})
|
||||
}
|
||||
63
apps/vscode/editor/src/utils/rpc.ts
Normal file
63
apps/vscode/editor/src/utils/rpc.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { uniqueId } from 'tldraw'
|
||||
import type { VscodeMessage, VscodeMessagePairs } from '../../../messages'
|
||||
import { vscode } from './vscode'
|
||||
|
||||
interface SimpleRpcOpts {
|
||||
timeout: number
|
||||
}
|
||||
class SimpleRpcError extends Error {
|
||||
id: string
|
||||
data: any
|
||||
constructor(id: keyof VscodeMessagePairs, data: any) {
|
||||
super(`Failed ${id}`)
|
||||
this.id = id
|
||||
this.data = data
|
||||
}
|
||||
}
|
||||
|
||||
export function rpc<T extends keyof VscodeMessagePairs>(
|
||||
id: T,
|
||||
data: VscodeMessagePairs[T]['request']['data'],
|
||||
opts: SimpleRpcOpts = { timeout: 5 * 1000 }
|
||||
) {
|
||||
const { timeout } = opts
|
||||
type RequestType = VscodeMessagePairs[T]['request']
|
||||
type ResponseType = VscodeMessagePairs[T]['response']
|
||||
type ErrorType = VscodeMessagePairs[T]['error']
|
||||
|
||||
const type = (id + '/request') as RequestType['type']
|
||||
const uuid = uniqueId()
|
||||
return new Promise<ResponseType['data']>((resolve, reject) => {
|
||||
const inMessage: VscodeMessage = {
|
||||
uuid,
|
||||
type,
|
||||
data,
|
||||
}
|
||||
vscode.postMessage(inMessage)
|
||||
|
||||
const handler = ({ data: response }: MessageEvent<ResponseType | ErrorType>) => {
|
||||
// Only handle messages that are meant to be a direct response to the message we sent
|
||||
if (response.uuid !== uuid + '_response') {
|
||||
return
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
window.removeEventListener('message', handler)
|
||||
}
|
||||
|
||||
if (response.type === `${id}/response`) {
|
||||
cleanup()
|
||||
resolve(response.data as ResponseType['data'])
|
||||
}
|
||||
if (response.type === `${id}/error`) {
|
||||
cleanup()
|
||||
reject(new SimpleRpcError(id, response.data as ErrorType['data']))
|
||||
}
|
||||
setTimeout(() => {
|
||||
cleanup()
|
||||
reject(new SimpleRpcError(id, { timeout: true }))
|
||||
}, timeout)
|
||||
}
|
||||
window.addEventListener('message', handler)
|
||||
})
|
||||
}
|
||||
8
apps/vscode/editor/src/utils/url.ts
Normal file
8
apps/vscode/editor/src/utils/url.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { vscode } from './vscode'
|
||||
|
||||
export function openUrl(url: string) {
|
||||
vscode.postMessage({
|
||||
type: 'vscode:open-window',
|
||||
data: { url, target: '_blank' },
|
||||
})
|
||||
}
|
||||
9
apps/vscode/editor/src/utils/vscode.ts
Normal file
9
apps/vscode/editor/src/utils/vscode.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
// @ts-ignore
|
||||
import type { VscodeMessage } from '../../../messages'
|
||||
|
||||
// Will be placed in global scope by extension
|
||||
declare function acquireVsCodeApi(): {
|
||||
postMessage(options: VscodeMessage): void
|
||||
}
|
||||
|
||||
export const vscode = acquireVsCodeApi()
|
||||
26
apps/vscode/editor/tsconfig.json
Normal file
26
apps/vscode/editor/tsconfig.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"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,
|
||||
"noUncheckedSideEffectImports": false,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"composite": true,
|
||||
"importHelpers": false,
|
||||
"skipDefaultLibCheck": true
|
||||
},
|
||||
"include": ["src", "../messages", "scripts"],
|
||||
"references": [{ "path": "../../../packages/assets" }, { "path": "../../../packages/tldraw" }]
|
||||
}
|
||||
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" }]
|
||||
}
|
||||
120
apps/vscode/messages.ts
Normal file
120
apps/vscode/messages.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
interface BookmarkRequest {
|
||||
type: 'vscode:bookmark/request'
|
||||
uuid: string
|
||||
data: {
|
||||
url: string
|
||||
}
|
||||
}
|
||||
interface BookmarkResponse {
|
||||
type: 'vscode:bookmark/response'
|
||||
uuid: string
|
||||
data: {
|
||||
url: string
|
||||
title?: string
|
||||
description?: string
|
||||
image?: string
|
||||
favicon?: string
|
||||
}
|
||||
}
|
||||
|
||||
interface BookmarkError {
|
||||
type: 'vscode:bookmark/error'
|
||||
uuid: string
|
||||
data: {
|
||||
error: string
|
||||
}
|
||||
}
|
||||
|
||||
interface GetFileRequest {
|
||||
type: 'vscode:get-file/request'
|
||||
uuid: string
|
||||
data: {
|
||||
url: string
|
||||
}
|
||||
}
|
||||
|
||||
interface GetFileResponse {
|
||||
type: 'vscode:get-file/response'
|
||||
uuid: string
|
||||
data: {
|
||||
fileName: string
|
||||
file: number[]
|
||||
mimeType: string
|
||||
}
|
||||
}
|
||||
|
||||
interface GetFileError {
|
||||
type: 'vscode:get-file/error'
|
||||
uuid: string
|
||||
data: {
|
||||
error: string
|
||||
}
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface VscodeMessagePairs {
|
||||
'vscode:bookmark': { request: BookmarkRequest; response: BookmarkResponse; error: BookmarkError }
|
||||
'vscode:get-file': { request: GetFileRequest; response: GetFileResponse; error: GetFileError }
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export type VscodeMessage =
|
||||
| {
|
||||
type: 'vscode:editor-loaded'
|
||||
}
|
||||
| {
|
||||
type: 'vscode:ready-to-receive-file'
|
||||
}
|
||||
| {
|
||||
type: 'vscode:v1-file-opened'
|
||||
data: {
|
||||
description: string
|
||||
backup: string
|
||||
backupSaved: string
|
||||
backupFailed: string
|
||||
dontAskAgain: string
|
||||
open: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
type: 'vscode:undo'
|
||||
}
|
||||
| {
|
||||
type: 'vscode:redo'
|
||||
}
|
||||
| {
|
||||
type: 'vscode:cancel-v1-migrate'
|
||||
}
|
||||
| {
|
||||
type: 'vscode:editor-updated' | 'vscode:revert'
|
||||
data: {
|
||||
fileContents: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
type: 'vscode:open-window'
|
||||
data: {
|
||||
url: string
|
||||
target: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
type: 'vscode:opened-file'
|
||||
data: {
|
||||
assetSrc: string
|
||||
fileContents: string
|
||||
showV1FileOpenWarning: boolean
|
||||
uri: string
|
||||
userId: string
|
||||
isDarkMode: boolean
|
||||
}
|
||||
}
|
||||
| {
|
||||
type: 'vscode:hide-v1-file-open-warning'
|
||||
}
|
||||
| { type: 'vscode:refresh-page' }
|
||||
| { type: 'vscode:hard-reset' }
|
||||
| BookmarkRequest
|
||||
| BookmarkResponse
|
||||
| GetFileRequest
|
||||
| GetFileResponse
|
||||
Reference in New Issue
Block a user